loadwave

package
v0.1.0 Latest Latest
Warning

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

Go to latest
Published: Aug 18, 2026 License: AGPL-3.0 Imports: 23 Imported by: 0

Documentation

Overview

Package loadwave is the public API for writing LoadWave load tests in Go.

A test is a normal Go program. It registers one or more scenarios and hands control to the runner, which turns the binary into a complete LoadWave node: it can run a test standalone, act as a coordinator serving the dashboard, or join an existing cluster as an agent.

package main

import (
    "context"
    "net/http"
    "time"

    "github.com/SnowyFoxStudios/LoadWave/pkg/loadwave"
    "github.com/SnowyFoxStudios/LoadWave/pkg/loadwave/run"
)

func main() {
    loadwave.Register(loadwave.Scenario{
        Name:   "browse",
        Weight: 3,
        Run:    browse,
    })
    run.Main()
}

func browse(ctx context.Context, vu *loadwave.VU) error {
    resp, err := vu.HTTP().Get(ctx, "/api/products")
    if err != nil {
        return err
    }
    vu.Check("products ok", resp.StatusCode == http.StatusOK)
    vu.ThinkBetween(ctx, time.Second, 3*time.Second)
    return nil
}

Concurrency

Each virtual user runs on its own goroutine and owns its VU exclusively, so scenario code needs no locking for anything reached through the VU. Anything a scenario shares between users — a package-level map, a fixture slice being mutated — is the scenario's own problem to synchronise.

Metrics

The HTTP client records the standard metric set automatically. Scenarios can add their own through VU.Metrics, and should attach tags through VU.Tag rather than encoding values into metric names. Tag values must come from a small fixed set: every distinct combination becomes a time series held in memory on the coordinator for the length of the run.

Testing scenarios

NewVU is exported so a scenario can be exercised from an ordinary Go test against an httptest.Server, with no coordinator, agent or worker involved.

Index

Constants

View Source
const (
	DefaultHTTPTimeout         = 30 * time.Second
	DefaultMaxIdleConnsPerHost = 512
	DefaultMaxRedirects        = 10
	DefaultMaxBodyBytes        = 4 << 20 // 4 MiB
)

Defaults applied when the corresponding HTTPOptions field is zero.

View Source
const (
	// MetricIterations counts completed scenario iterations.
	MetricIterations = "iterations"
	// MetricIterationDuration is the wall time of one full iteration, in
	// milliseconds, excluding time the VU spent in Think.
	MetricIterationDuration = "iteration_duration"
	// MetricIterationFailed is the share of iterations that returned an error.
	MetricIterationFailed = "iteration_failed"
	// MetricVUs is the number of virtual users currently executing.
	MetricVUs = "vus"

	// MetricHTTPReqs counts HTTP requests issued.
	MetricHTTPReqs = "http_reqs"
	// MetricHTTPReqDuration is total request time in milliseconds, from the
	// start of the request to the last byte of the body.
	MetricHTTPReqDuration = "http_req_duration"
	// MetricHTTPReqWaiting is time to first byte in milliseconds: the server's
	// own think time, with connection setup and body transfer excluded.
	MetricHTTPReqWaiting = "http_req_waiting"
	// MetricHTTPReqConnecting is time spent establishing a TCP connection, in
	// milliseconds. Zero on a reused connection.
	MetricHTTPReqConnecting = "http_req_connecting"
	// MetricHTTPReqTLS is time spent on the TLS handshake, in milliseconds.
	MetricHTTPReqTLS = "http_req_tls_handshaking"
	// MetricHTTPReqFailed is the share of requests judged unsuccessful.
	MetricHTTPReqFailed = "http_req_failed"
	// MetricHTTPReqBytesIn counts response bytes read.
	MetricHTTPReqBytesIn = "http_req_bytes_in"
	// MetricHTTPReqBytesOut counts request bytes written.
	MetricHTTPReqBytesOut = "http_req_bytes_out"

	// MetricChecks is the share of checks that passed.
	MetricChecks = "checks"
	// MetricErrors counts errors reported by scenarios via VU.Fail.
	MetricErrors = "errors"
)

Built-in metric names.

Scenarios are free to emit their own metrics alongside these, but the dashboard and the default threshold set are written against these names, so custom HTTP-like protocols are best served by reusing them.

View Source
const (
	// LabelScenario is the name of the scenario that produced the observation.
	LabelScenario = "scenario"
	// LabelName is the call site's stable identity. For HTTP this is the URL
	// with high-cardinality path segments collapsed, so that /users/1 and
	// /users/2 aggregate into one series instead of two million.
	LabelName = "name"
	// LabelMethod is the HTTP method.
	LabelMethod = "method"
	// LabelStatus is the HTTP status code as a string, or "0" if the request
	// never produced a response.
	LabelStatus = "status"
	// LabelError is a short, bounded classification of a transport failure —
	// "timeout", "connection_refused" and the like. Never a raw error string,
	// which would blow up series cardinality.
	LabelError = "error"
	// LabelCheck is the name given to a check.
	LabelCheck = "check"
	// LabelExpected marks whether a failure was anticipated by the scenario.
	LabelExpected = "expected"
)

Standard label keys. Sticking to these keeps scenario metrics legible in the dashboard, which groups and filters on them.

View Source
const DefaultBetweenRequests = time.Second

DefaultBetweenRequests is the pause inserted after every request when a run does not say otherwise.

It is deliberately not zero. A scenario with no explicit think time will otherwise loop as fast as the network allows, and a scenario whose first request fails instantly — a refused connection, a 500 returned from a cache — loops as fast as the CPU allows. Both bury the system under test in traffic no real population would generate, and the second is how a load test turns into an accidental denial of service against a service that is already down.

One second is roughly a real person's pace and easy to reason about. Set it to zero explicitly for a throughput test, where flat out is the point.

View Source
const MaxFailureMessage = 240

MaxFailureMessage bounds how much of a response body or error string is kept as a failure excerpt.

This is a hint for a human reading the dashboard, not a payload. A server that answers a load test with a stack trace should not be able to turn the control plane into a log shipper.

Variables

View Source
var Default = NewRegistry()

Default is the registry used by the package-level Register function and by the runner when a binary does not supply its own.

Functions

func DefaultIsSuccess

func DefaultIsSuccess(r *Response) bool

DefaultIsSuccess treats a response as successful when the transport succeeded and the status is below 400.

func DeriveRequestName

func DeriveRequestName(method, path string) string

DeriveRequestName builds a low-cardinality metric label from a method and path by collapsing segments that look like identifiers.

Without this, a run against /orders/{id} produces one time series per order and the dashboard becomes unreadable long before the coordinator runs out of memory. The heuristic is deliberately conservative; scenarios that need precision should set Request.Name.

func Register

func Register(s Scenario)

Register adds a scenario to the default registry, panicking on error.

This is the entry point for the common case:

func init() {
    loadwave.Register(loadwave.Scenario{
        Name: "browse",
        Run:  browse,
    })
}

func Slice

func Slice[T any](s Shard, items []T) []T

Slice returns the elements of items belonging to this shard, preserving order. The result aliases nothing: callers may mutate it freely.

func StateOf

func StateOf[T any](vu *VU, key string) (T, bool)

StateOf is the generic form of State, returning the zero value of T when the key is absent or holds a different type.

func TruncateMessage

func TruncateMessage(text string) string

TruncateMessage reduces arbitrary text to a short single-line excerpt.

Response bodies are HTML pages, JSON documents and stack traces; rendered verbatim they would wreck the table they appear in. Collapsing whitespace and clipping to a fixed length keeps a row readable while preserving the part that usually identifies the problem, which is nearly always at the front.

Types

type Failure

type Failure struct {
	// Name is the request's metric name.
	Name string
	// Method is the HTTP method.
	Method string
	// Status is the HTTP status, or 0 when no response arrived.
	Status int
	// ErrorClass is the transport failure classification, empty when a
	// response was received.
	ErrorClass string
	// Message is a short excerpt of what went wrong: the response body, or
	// the transport error's text.
	Message string
}

Failure describes one request that did not succeed.

Every field except Message is bounded by construction: Name is already collapsed to low cardinality, and ErrorClass comes from a fixed vocabulary. That is what lets failures be aggregated rather than streamed.

type FailureReporter

type FailureReporter interface {
	ReportFailure(Failure)
}

FailureReporter receives details of failed requests.

It is a separate, optional interface rather than part of Recorder because a failure is not a number: it carries text, it is aggregated differently, and most Recorder implementations have no use for it. A recorder that does not implement this simply receives no samples.

type HTTPClient

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

HTTPClient issues requests on behalf of a single virtual user and records the standard HTTP metrics for each one.

func (*HTTPClient) Delete

func (c *HTTPClient) Delete(ctx context.Context, rawURL string) (*Response, error)

Delete issues a DELETE request.

func (*HTTPClient) Do

func (c *HTTPClient) Do(ctx context.Context, req Request) (*Response, error)

Do issues the request and records its metrics.

The returned Response is never nil. The returned error is non-nil only for transport-level failures; a 4xx or 5xx response returns a nil error with the status set, because at load-test altitude a 500 is a measurement, not an exception. Scenarios that treat bad statuses as failures should say so with a check or by returning their own error.

func (*HTTPClient) Get

func (c *HTTPClient) Get(ctx context.Context, rawURL string) (*Response, error)

Get issues a GET request.

func (*HTTPClient) PostJSON

func (c *HTTPClient) PostJSON(ctx context.Context, rawURL string, body any) (*Response, error)

PostJSON issues a POST request with a JSON body.

func (*HTTPClient) PutJSON

func (c *HTTPClient) PutJSON(ctx context.Context, rawURL string, body any) (*Response, error)

PutJSON issues a PUT request with a JSON body.

type HTTPClientFactory

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

HTTPClientFactory builds one HTTPClient per virtual user.

It exists so that the expensive, shareable part — the transport and its connection pool — is built once per worker process, while the cheap per-user part is built ten thousand times.

func NewHTTPClientFactory

func NewHTTPClientFactory(opts HTTPOptions) (*HTTPClientFactory, error)

NewHTTPClientFactory validates the options and prepares the shared state.

func (*HTTPClientFactory) Close

func (f *HTTPClientFactory) Close()

Close releases the shared transport's pooled connections.

func (*HTTPClientFactory) New

func (f *HTTPClientFactory) New() *HTTPClient

New returns a client for one virtual user.

type HTTPOptions

type HTTPOptions struct {
	// BaseURL is prefixed to relative request paths.
	BaseURL string

	// Timeout bounds a whole request, including body transfer. Zero applies
	// DefaultHTTPTimeout; a load test with no timeout will eventually wedge
	// every VU behind one unresponsive endpoint.
	Timeout time.Duration

	// Headers are sent with every request. Per-request headers win on
	// conflict.
	Headers http.Header

	// UserAgent overrides the default User-Agent header.
	UserAgent string

	// InsecureSkipTLSVerify disables certificate validation. Load
	// environments frequently use self-signed certificates; production ones
	// should not.
	InsecureSkipTLSVerify bool

	// MaxIdleConnsPerHost caps pooled idle connections per host. Go's default
	// is 2, which throttles a load test to a trickle of connection churn and
	// makes it measure the client rather than the server. Zero applies
	// DefaultMaxIdleConnsPerHost.
	MaxIdleConnsPerHost int

	// DisableKeepAlives forces a fresh connection per request, which measures
	// connection setup cost as well as request cost.
	DisableKeepAlives bool

	// DisableCompression stops the transport requesting gzip.
	DisableCompression bool

	// FollowRedirects makes the client follow 3xx responses. Off by default:
	// a load test usually wants to measure the redirect itself.
	FollowRedirects bool

	// MaxRedirects caps redirect depth when FollowRedirects is set. Zero
	// applies DefaultMaxRedirects.
	MaxRedirects int

	// IsolatePerVU gives every virtual user its own connection pool, so each
	// behaves like a distinct client. More faithful, but costs a file
	// descriptor per VU per host and will hit ulimits at high VU counts.
	// Off by default: VUs share one pool.
	IsolatePerVU bool

	// DiscardBody streams response bodies to nowhere instead of buffering
	// them. Bytes are still counted. Use it when scenarios never inspect
	// bodies and responses are large.
	DiscardBody bool

	// MaxBodyBytes caps how much of a response body is buffered. Zero applies
	// DefaultMaxBodyBytes. Bytes beyond the cap are read and counted but
	// discarded, so the server still does the full work.
	MaxBodyBytes int64

	// Trace collects connection-level timings — time to first byte, connect
	// and TLS handshake duration — via httptrace. Costs a few hundred
	// nanoseconds per request. On by default.
	Trace *bool

	// Proxy is an optional proxy URL. Empty uses the environment's proxy
	// settings.
	Proxy string

	// IsSuccess decides whether a response counts toward the failure rate.
	// The default treats a 2xx or 3xx status with no transport error as
	// success.
	IsSuccess func(*Response) bool

	// BetweenRequests pauses after every request, whatever its outcome.
	//
	// This is the run's pacing floor. Without it a scenario with no explicit
	// think time loops as fast as the network allows, and one whose request
	// fails instantly loops as fast as the CPU allows — which is how a load
	// test becomes an accidental denial of service against a service that has
	// already fallen over.
	//
	// The zero value applies DefaultBetweenRequests. Use NoBetweenRequests to
	// mean genuinely none, which is what a pure throughput test wants.
	BetweenRequests Pause

	// NoBetweenRequests disables pacing entirely, overriding BetweenRequests.
	//
	// A separate flag rather than a zero duration because zero has to keep
	// meaning "not configured": a run that never mentions pacing should get
	// the safe default, not flat out.
	NoBetweenRequests bool
}

HTTPOptions configures how a run talks HTTP. Defaults are tuned for load generation rather than for a typical application client.

type Labels

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

Labels is an immutable, pre-hashed set of metric tags.

Metric tags are attached to every single observation, so building them has to be cheap. Labels are meant to be constructed once — when a scenario is set up, or when an HTTP call site is first seen — and then reused for the millions of samples that follow. Because the value is immutable, the hash can be computed once at construction and reused for every map lookup.

The zero Labels is valid and carries no tags.

func LabelsFromMap

func LabelsFromMap(m map[string]string) Labels

LabelsFromMap builds a Labels from a map, for use at boundaries where tags arrive as maps — configuration files and protobuf messages.

func NewLabels

func NewLabels(kv ...string) Labels

NewLabels builds a Labels from alternating key/value arguments.

It panics if given an odd number of arguments, because that is always a programming error at a call site rather than a runtime condition worth propagating. Later duplicate keys overwrite earlier ones.

func (Labels) All

func (l Labels) All(yield func(key, value string) bool)

All iterates the pairs in key order.

func (Labels) Equal

func (l Labels) Equal(other Labels) bool

Equal reports whether two label sets carry exactly the same pairs.

func (Labels) Get

func (l Labels) Get(key string) (string, bool)

Get returns the value for a key and whether it was present.

func (Labels) Hash

func (l Labels) Hash() uint64

Hash returns the precomputed hash of the label set. Callers must still compare with Equal before treating two label sets as identical.

func (Labels) Len

func (l Labels) Len() int

Len reports how many key/value pairs the set holds.

func (Labels) Map

func (l Labels) Map() map[string]string

Map materialises the labels as a map. It allocates, so it belongs on reporting paths — serialising a batch, rendering the UI — never on the per-request hot path.

func (Labels) String

func (l Labels) String() string

String renders the labels as `k=v,k=v` in key order. Intended for logs and test failure messages.

func (Labels) With

func (l Labels) With(kv ...string) Labels

With returns a copy of l with the given key/value pairs added or replaced. The receiver is never modified, so a Labels value may be shared freely across virtual users without synchronisation.

type MetricKind

type MetricKind uint8

MetricKind determines how observations for a metric are aggregated, both within a node and when deltas from many nodes are merged centrally.

const (
	// KindCounter accumulates a monotonically increasing total, such as the
	// number of requests issued. Merging adds.
	KindCounter MetricKind = iota + 1

	// KindGauge records a value that goes up and down, such as the number of
	// active virtual users. Merging adds across nodes at the same instant,
	// but never across instants.
	KindGauge

	// KindTrend records a distribution and reports percentiles. Merging is
	// done through an HDR histogram, which is why a p99 stays correct when a
	// run is spread over many machines.
	KindTrend

	// KindRate records the fraction of observations that were true, such as
	// the share of requests that failed. Merging adds both the numerator and
	// the denominator.
	KindRate
)

func (MetricKind) String

func (k MetricKind) String() string

String implements fmt.Stringer.

type Pause

type Pause struct {
	Min time.Duration
	Max time.Duration
}

Pause is a delay, optionally drawn uniformly from a range.

The zero Pause means no delay.

func NewPause

func NewPause(d time.Duration) Pause

NewPause returns a fixed-length pause.

func NewPauseRange

func NewPauseRange(minDur, maxDur time.Duration) Pause

NewPauseRange returns a pause drawn uniformly from [minDur, maxDur].

Prefer a range over a fixed value. Identical pauses make every virtual user march in lockstep, which produces traffic in synchronised bursts rather than the smooth arrival pattern a real population generates — and the bursts are what your service ends up being measured against.

func ParsePause

func ParsePause(spec string) (Pause, error)

ParsePause reads a fixed delay or a range: "500ms", "1s", "1s-3s".

An empty string is an error rather than a zero pause: at every call site the difference between "not specified" and "explicitly none" matters, and only the caller knows which an empty field means.

func (Pause) Duration

func (p Pause) Duration(rnd *rand.Rand) time.Duration

Duration draws a delay from the pause.

func (Pause) IsZero

func (p Pause) IsZero() bool

IsZero reports whether the pause is no delay at all.

func (Pause) String

func (p Pause) String() string

String renders the pause the way it is written in configuration.

type Recorder

type Recorder interface {
	// Count adds delta to a counter.
	Count(metric string, labels Labels, delta float64)
	// Trend records one observation in a distribution.
	Trend(metric string, labels Labels, value float64)
	// Rate records one boolean observation.
	Rate(metric string, labels Labels, ok bool)
	// Gauge sets the current value of a gauge.
	Gauge(metric string, labels Labels, value float64)
}

Recorder is the sink a scenario writes observations to.

The engine supplies the implementation; scenarios only ever consume it via VU.Metrics. It is defined here, in the public package, so that the engine can depend on the SDK rather than the other way round.

Implementations must be safe for concurrent use: many virtual users share one Recorder.

func DiscardRecorder

func DiscardRecorder() Recorder

DiscardRecorder is a Recorder that drops every observation.

type Registry

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

Registry holds the scenarios a binary knows how to execute.

Registration happens at startup, before any run begins, but the registry is still guarded by a mutex: scenarios are commonly registered from init functions across several files, and a data race there would be a miserable thing to debug.

func NewRegistry

func NewRegistry() *Registry

NewRegistry returns an empty registry. Most programs use the package-level Default registry instead of constructing their own; an explicit registry is useful in tests, where global state between cases is a hazard.

func (*Registry) Clone

func (r *Registry) Clone() *Registry

Clone returns an independent registry holding the same scenarios.

Workers build one of these per run and add the run's declarative scenarios to the copy, so that a configuration's YAML-defined scenarios do not leak into the next run started against the same process.

func (*Registry) Len

func (r *Registry) Len() int

Len reports how many scenarios are registered.

func (*Registry) Lookup

func (r *Registry) Lookup(name string) (Scenario, bool)

Lookup returns a scenario by name.

func (*Registry) MustRegister

func (r *Registry) MustRegister(s Scenario)

MustRegister is Register, panicking on error.

This is the right call from an init function or from main, where a duplicate or malformed scenario is a bug that should stop the program immediately rather than surface as a confusing empty run much later.

func (*Registry) Names

func (r *Registry) Names() []string

Names lists every registered scenario, sorted, so that CLI output and run sharding are deterministic.

func (*Registry) Register

func (r *Registry) Register(s Scenario) error

Register adds a scenario, returning an error if it is invalid or if the name is already taken.

type Request

type Request struct {
	// Method defaults to GET.
	Method string

	// URL is absolute, or a path resolved against the run's base URL.
	URL string

	// Name is the metric label for this call site. When empty, it is derived
	// from the method and path with variable segments collapsed to `*`, so
	// /users/1 and /users/2 share one series. Set it explicitly whenever the
	// derived name would still be high-cardinality.
	Name string

	// Header is merged over the run-wide headers.
	Header http.Header

	// Query parameters appended to the URL.
	Query url.Values

	// Body is the raw request body. Mutually exclusive with JSON and Form.
	Body []byte

	// JSON is marshalled as the body, with a JSON content type.
	JSON any

	// Form is encoded as the body, with a form content type.
	Form url.Values

	// Timeout overrides the run-wide timeout for this request.
	Timeout time.Duration

	// Tags are added to this request's metrics.
	Tags Labels

	// ExpectStatus lists the acceptable status codes. When set, it replaces
	// the run's success predicate for this request.
	ExpectStatus []int

	// BetweenRequests overrides the run's pacing for this one request. A
	// pointer to the zero Pause means no pause at all; nil means use the
	// run's default.
	BetweenRequests *Pause
}

Request describes one HTTP call.

type Response

type Response struct {
	// StatusCode is the HTTP status, or 0 when no response was received.
	StatusCode int
	Status     string
	Proto      string
	Header     http.Header

	// Body holds the response body, empty when HTTPOptions.DiscardBody is set
	// or the body exceeded MaxBodyBytes.
	Body []byte

	// Truncated reports that the body was longer than MaxBodyBytes.
	Truncated bool

	// Duration is the whole request, from first byte written to last byte read.
	Duration time.Duration
	// TTFB is the wait for the first response byte.
	TTFB time.Duration
	// Connecting is TCP setup time, zero when the connection was reused.
	Connecting time.Duration
	// TLSHandshake is handshake time, zero for plaintext or a reused connection.
	TLSHandshake time.Duration
	// ConnReused reports whether a pooled connection served this request.
	ConnReused bool

	BytesIn  int64
	BytesOut int64

	// Err is the transport error, nil when a response was received. An HTTP
	// 500 is not an error here; it is a successful exchange with a bad status.
	Err error
}

Response is the outcome of a Request. It is always non-nil, including when the transport failed, so scenarios can branch on Err or OK without a nil check first.

func (*Response) JSON

func (r *Response) JSON(v any) error

JSON unmarshals the response body into v.

func (*Response) OK

func (r *Response) OK() bool

OK reports whether the request completed with a status below 400.

func (*Response) String

func (r *Response) String() string

String renders a compact summary, for logs and check messages.

func (*Response) Text

func (r *Response) Text() string

Text returns the body as a string.

type Scenario

type Scenario struct {
	// Name identifies the scenario in metrics, the CLI and the dashboard.
	// Must match [a-zA-Z0-9][a-zA-Z0-9_.-]{0,63}.
	Name string

	// Weight is this scenario's relative share of iterations when a run
	// executes several scenarios at once. A scenario with weight 3 runs three
	// times as often as one with weight 1. Zero is treated as 1.
	Weight int

	// Description is shown in the dashboard when picking scenarios.
	Description string

	// Setup prepares process-wide state, such as authenticating a service
	// account whose token every VU will share. An error here fails the run on
	// this worker before any load is generated.
	Setup func(ctx context.Context) error

	// Teardown releases whatever Setup acquired. It is called even when the
	// run failed, and is given the plan's graceful-stop budget to finish.
	Teardown func(ctx context.Context) error

	// OnVUStart initialises per-user state — logging a distinct user in,
	// picking a row from a fixture file. Store it on the VU with VU.SetState.
	OnVUStart func(ctx context.Context, vu *VU) error

	// OnVUStop cleans up per-user state.
	OnVUStop func(ctx context.Context, vu *VU) error

	// Run executes one iteration. Returning an error marks the iteration
	// failed and increments the error metrics; it does not stop the run.
	Run func(ctx context.Context, vu *VU) error
}

Scenario is a named unit of simulated user behaviour.

Run is the only required field. It is called once per iteration, by every virtual user assigned to the scenario, until the run's load profile says to stop. It should represent one pass of whatever a real user would do — browsing a page, completing a checkout — and it should return an error when that pass did not succeed, since that is what drives the failure rate.

The lifecycle hooks fire in this order:

Setup           once per worker process, before any VU starts
  OnVUStart     once per virtual user
    Run         repeatedly, until the profile ends
  OnVUStop      once per virtual user
Teardown        once per worker process, after all VUs have stopped

Setup and Teardown run once per worker process, not once per run. A run spread over four processes calls Setup four times. Anything that must happen exactly once for the whole run — seeding a database, say — belongs outside the scenario.

func (Scenario) EffectiveWeight

func (s Scenario) EffectiveWeight() int

EffectiveWeight resolves the zero value to 1.

func (Scenario) Validate

func (s Scenario) Validate() error

Validate reports whether the scenario is well formed.

type Shard

type Shard struct {
	Index uint32
	Count uint32
}

Shard tells a virtual user which slice of shared test data belongs to it.

A distributed run must not have every node hammering the same fixture row, and coordinating that at runtime would mean chatter on the hot path. Instead the coordinator hands each node a static (Index, Count) pair at start, and nodes partition data arithmetically with no further communication.

func (Shard) Owns

func (s Shard) Owns(i int) bool

Owns reports whether this shard is responsible for item i.

type VU

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

VU is one virtual user: a single simulated client, executing its scenario in a loop for the lifetime of the run.

Exactly one goroutine ever touches a given VU, so nothing on it is synchronised and scenarios may store whatever they like on it without locking. Do not hand a VU to a goroutine you spawn yourself; if a scenario needs concurrency within one iteration, share only immutable values.

func NewVU

func NewVU(cfg VUConfig) *VU

NewVU constructs a virtual user.

func (*VU) BeginIteration

func (vu *VU) BeginIteration(n int)

BeginIteration resets per-iteration state. The engine calls this; scenarios do not.

func (*VU) Check

func (vu *VU) Check(name string, ok bool) bool

Check records a named assertion and reports the result back to the caller, so it composes into control flow:

if !vu.Check("logged in", resp.StatusCode == 200) {
    return fmt.Errorf("login failed: %d", resp.StatusCode)
}

A failing check does not by itself fail the iteration. Return an error from the scenario to do that.

func (*VU) Checkf

func (vu *VU) Checkf(name string, ok bool, format string, args ...any) bool

Checkf is Check with a message logged when the assertion fails. The message is not used as a metric label, so it may safely include specific values.

func (*VU) Close

func (vu *VU) Close() error

Close releases resources held by the VU, such as idle HTTP connections.

func (*VU) EndIteration

func (vu *VU) EndIteration(elapsed time.Duration, err error)

EndIteration reports the accounting for the iteration just finished: the time it took excluding think time, and whether it failed. The engine calls this; scenarios do not.

func (*VU) Fail

func (vu *VU) Fail(err error)

Fail records an error against this iteration. Returning an error from the scenario's Run does the same thing; Fail is for recording an additional error without abandoning the iteration.

func (*VU) HTTP

func (vu *VU) HTTP() *HTTPClient

HTTP returns the VU's HTTP client.

It panics if the run was configured without one, which only happens in hand-built test VUs; that is a clearer failure than a nil dereference deep inside a scenario.

func (*VU) ID

func (vu *VU) ID() int64

ID returns the run-wide unique identifier of this virtual user.

func (*VU) Index

func (vu *VU) Index() int

Index returns the VU's position within its worker process.

func (*VU) Iteration

func (vu *VU) Iteration() int

Iteration returns the zero-based index of the current iteration.

func (*VU) Labels

func (vu *VU) Labels() Labels

Labels returns the tags currently applied to this VU's observations.

func (*VU) Log

func (vu *VU) Log() *slog.Logger

Log returns a logger already tagged with the VU id and scenario.

func (*VU) Metrics

func (vu *VU) Metrics() Recorder

Metrics returns the recorder, for scenarios emitting custom metrics.

func (*VU) Rand

func (vu *VU) Rand() *rand.Rand

Rand returns this VU's random source. It is not shared with any other VU, so it needs no locking and contributes no contention.

func (*VU) Scenario

func (vu *VU) Scenario() string

Scenario returns the name of the scenario being executed.

func (*VU) SetState

func (vu *VU) SetState(key string, value any)

SetState stores a value that survives across iterations of this VU.

func (*VU) Shard

func (vu *VU) Shard() Shard

Shard returns the data partition assigned to this VU.

func (*VU) State

func (vu *VU) State(key string) (any, bool)

State retrieves a value stored by SetState.

func (*VU) Tag

func (vu *VU) Tag(key, value string)

Tag adds a label to every metric this VU emits for the rest of the current iteration. It is reset when the iteration ends.

Keep the value space small. A tag with unbounded values — a user id, a timestamp — creates a new time series per value and will exhaust memory on the coordinator.

func (*VU) Think

func (vu *VU) Think(ctx context.Context, d time.Duration)

Think pauses the virtual user, simulating a real person reading the page.

The pause is interruptible: when the run is stopping, Think returns early rather than holding the shutdown open. Time spent here is excluded from iteration_duration, so think time does not distort the metric.

func (*VU) ThinkBetween

func (vu *VU) ThinkBetween(ctx context.Context, minDur, maxDur time.Duration)

ThinkBetween pauses for a duration drawn uniformly from [minDur, maxDur]. Constant think times make virtual users march in lockstep and produce artificial traffic spikes; jitter is almost always what you want.

type VUConfig

type VUConfig struct {
	// ID is unique across the entire run, not just this process. The
	// coordinator allocates a distinct range to every worker.
	ID int64
	// Index is this VU's position within its worker process, from 0.
	Index int
	// Shard identifies which slice of shared fixtures this VU should use.
	Shard Shard
	// Scenario is the name of the scenario this VU executes.
	Scenario string
	// Recorder receives all metric observations. Defaults to discarding.
	Recorder Recorder
	// HTTP is the client scenarios reach through VU.HTTP.
	HTTP *HTTPClient
	// Logger receives scenario log output. Defaults to slog.Default.
	Logger *slog.Logger
	// Rand seeds this VU's generator. Defaults to a per-VU deterministic
	// source derived from ID, which keeps runs reproducible.
	Rand *rand.Rand
	// Tags are attached to every metric this VU emits, on top of the
	// automatic scenario tag.
	Tags Labels
}

VUConfig is the set of dependencies needed to construct a VU.

The engine fills this in for real runs. It is exported so scenario authors can build a VU in their own unit tests and exercise scenario logic without standing up a coordinator.

Directories

Path Synopsis
Package run turns a Go program into a complete LoadWave binary.
Package run turns a Go program into a complete LoadWave binary.

Jump to

Keyboard shortcuts

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