Documentation
¶
Overview ¶
Package harness is the load generator the resilience and load suites share.
It is the RT-8 command's own generator, lifted out of it and given a ramp. Three properties are the reason it exists rather than a library being pulled in, and every one of them is a decision somebody has to be able to read:
- It counts from the client. What a server reports about itself under overload is what it managed to record, and a request that never got a worker never appears in it.
- It paces independently of the responses. A generator whose next request waits for the last answer measures itself: when the installation slows down, the offered load falls with it and the numbers stay beautiful. That is coordinated omission, and it is the single most common way a load test lies.
- It records a timeline and the class of every call. "P95 held over ten minutes" and "P95 held while the shedding was engaged" are different claims, and only the second one is evidence for RT-6.
Index ¶
- Constants
- type Baseline
- type Bucket
- type Class
- type Direction
- type Figure
- type Latency
- type Pacer
- type Plan
- type Recorder
- func (r *Recorder) Created() []string
- func (r *Recorder) LastClosed(now time.Time) Bucket
- func (r *Recorder) Observe(class Class, status int, took time.Duration, err error)
- func (r *Recorder) Remember(id string)
- func (r *Recorder) Shed(class Class) int
- func (r *Recorder) Summarise(ended time.Time) Summary
- func (r *Recorder) Throughput(from, to time.Duration) float64
- func (r *Recorder) Window(class Class, from, to time.Duration) Latency
- type Regression
- type Stage
- type Summary
- type Window
Constants ¶
const Tick = 5 * time.Second
Tick is the resolution of the timeline. Five seconds is fine enough to place an event - a rollout, the moment shedding engaged - inside a run, and coarse enough that the report stays readable.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type Baseline ¶
type Baseline struct {
Name string `json:"name"`
RecordedAt string `json:"recorded_at"`
// Hardware is the machine the figures were measured on, and it is load-bearing: comparing a
// run against figures from different iron measures the iron. A run on hardware this does not
// name records its measurement and compares nothing (observability-reliability.md §13.2).
Hardware string `json:"hardware"`
Dataset struct {
Tenants int `json:"tenants"`
Items int `json:"items"`
} `json:"dataset"`
Figures map[string]Figure `json:"figures"`
}
Baseline is a set of figures, recorded on a named machine over a named dataset.
func LoadBaseline ¶
LoadBaseline reads a baseline from the repository.
func (Baseline) Compare ¶
func (b Baseline) Compare(measured map[string]float64) (regressions []Regression, missing []string)
Compare answers the one question this tier is allowed to answer: did anything get significantly worse?
A figure the run did not measure is a miss rather than a pass - a guard that silently ignores what disappeared would go green on the day the measurement broke. A figure the run measured and the baseline does not carry is not an error: a new figure is added to the baseline when somebody has a number worth recording, not on the first run that produces one.
type Bucket ¶
type Bucket struct {
SecondsIn int `json:"seconds_in"`
OfferedRate int `json:"offered_rate"`
Requests int `json:"requests"`
NonSuccess int `json:"non_success"`
ServerErrors int `json:"server_errors"`
Shed int `json:"shed"`
TransportErrors int `json:"transport_errors"`
// InteractiveP95 is the number RT-6 is about, per interval rather than over the run: an
// average that holds while the middle three minutes were terrible is not evidence.
InteractiveP95 int64 `json:"interactive_p95_ms"`
// contains filtered or unexported fields
}
Bucket is one interval of the timeline.
type Class ¶
type Class string
Class is what a request is worth waiting for, and it is the same two-way split the load shedder makes (infrastructure/resilience). The harness has to know it because the whole of RT-6 is a statement about one class while the other is being refused.
type Direction ¶
type Direction string
Direction says which way is worse. Latency and memory rise; throughput falls. A figure without one would be a comparison that silently reads half of its regressions as improvements.
type Figure ¶
type Figure struct {
Value float64 `json:"value"`
Direction Direction `json:"direction"`
// BandPercent is how far the figure may move in the worse direction before it is a
// regression. It is per figure because the figures are not equally noisy: a P95 on a shared
// runner moves far more than a count of refusals does.
BandPercent float64 `json:"band_percent"`
// MinimumDelta is the smallest absolute movement worth calling a regression, and it exists
// because a percentage band is the wrong shape for a small number. A P95 of six milliseconds
// with a 60 % band is red at ten, and ten is what one garbage collection during the run looks
// like - the guard would report the machine and be turned off within a week. Both conditions
// have to hold: past the band *and* moved by at least this much.
MinimumDelta float64 `json:"minimum_delta,omitempty"`
// Note says what the figure is, in words, so that a red build is readable without this file
// beside the test that produced it.
Note string `json:"note,omitempty"`
}
Figure is one recorded number and the band around it.
type Latency ¶
type Latency struct {
Count int `json:"count"`
P50 int64 `json:"p50"`
P95 int64 `json:"p95"`
P99 int64 `json:"p99"`
Max int64 `json:"max"`
}
Latency is what a run felt like, not what it promised.
func Percentiles ¶
Percentiles sorts a copy rather than the caller's slice: the samples belong to a recorder that is still holding them, and sorting them in place would reorder a live buffer.
type Pacer ¶
type Pacer struct {
// contains filtered or unexported fields
}
Pacer hands out permission to make a request, at the plan's current rate, shared by every worker.
A rate held by the workers themselves would drift with latency: eight workers waiting 200 ms each is forty requests a second only while every answer is instant. This is the coordinated omission discipline, and it is why the permits are produced by the clock rather than by the answers.
type Plan ¶
type Plan []Stage
Plan is a ramp. One stage is a flat run, which is what RT-8 has always done; several stages are how capacity is found - the rate is raised until the answers stop keeping up, and the stage in which that happened is visible in the timeline.
Expressed as stages rather than as a start, an end and a slope because a ramp with a plateau is the useful shape: rise, hold, look. A slope alone never holds anything long enough for a P95 to mean something.
func (Plan) Peak ¶
Peak is the highest rate the plan offers. It is what a report calls the run's ceiling.
type Recorder ¶
type Recorder struct {
// contains filtered or unexported fields
}
Recorder collects what every worker sees.
One mutex rather than per-worker counters merged at the end: the timeline needs each observation placed in time, and the contention of a few thousand requests a minute is not worth a lock-free design.
func (*Recorder) LastClosed ¶
LastClosed is the interval that has just ended, and it never creates one. The progress ticker fires on the boundary, so the interval current at that moment is always the one nobody has written to yet - reporting it printed a run of zeroes through a run that was working.
func (*Recorder) Observe ¶
Observe records one answer. A status of zero with an error is a transport failure - a connection reset carries no status code at all, and a run that only counted 5xx would call that a success.
func (*Recorder) Remember ¶
Remember keeps an identifier the installation said it created, so that the run can go looking for all of them afterwards.
func (*Recorder) Shed ¶
Shed is how many calls of a class were refused. Read while the run is still going, which is what lets a test say the mechanism engaged before it says anything about latency.
func (*Recorder) Summarise ¶
Summarise closes the run. It takes no lock beyond its own: every worker is finished by the time a caller asks for this.
func (*Recorder) Throughput ¶
Throughput is how many calls the installation actually answered per second over a window, counting neither the refusals nor the connections that never got one.
It is the capacity figure and it only means something once the offered rate is past what the process can serve: below saturation this returns the offered rate, because that is what was asked of it. Which is why the capacity figure comes from an overload stage and not from a steady one - a run that kept up measured the client.
func (*Recorder) Window ¶
Window is the latency of one class over a slice of the run, pooled from the raw samples rather than averaged out of the per-interval percentiles. A percentile of percentiles is not a percentile of anything, and the stage comparison RT-6 rests on would be built out of one.
from is inclusive and to is exclusive, both measured from the start of the run. It exists so a ramp can be read stage by stage: the claim worth making is not "the P95 held over the run" but "the P95 under overload was within a factor of the P95 before it".
type Regression ¶
type Regression struct {
Figure string `json:"figure"`
Baseline float64 `json:"baseline"`
Measured float64 `json:"measured"`
// Allowed is the worst value that would still have passed - the edge of the band, so that a
// red build says how far past it the run landed rather than only that it was past.
Allowed float64 `json:"allowed"`
Note string `json:"note,omitempty"`
}
Regression is one figure that moved too far the wrong way.
func (Regression) String ¶
func (r Regression) String() string
type Stage ¶
type Stage struct {
// PerSecond is the offered rate over the whole generator, not per worker.
PerSecond int
// For is how long this step is held.
For time.Duration
}
Stage is one step of a ramp: an offered rate, held for a while.
type Summary ¶
type Summary struct {
StartedAt time.Time `json:"started_at"`
EndedAt time.Time `json:"ended_at"`
// OfferedPeak is the plan's ceiling. Recorded beside the achieved rate, because a run that
// offered 400 a second and achieved 120 is a capacity finding rather than a failed run.
OfferedPeak int `json:"offered_peak_per_second"`
Requests int `json:"requests"`
ByStatus map[string]int `json:"by_status"`
Shed map[string]int `json:"shed_by_class"`
TransportErrors int `json:"transport_errors"`
ErrorExamples []string `json:"transport_error_examples,omitempty"`
Latency map[string]Latency `json:"latency_ms_by_class"`
Timeline []Bucket `json:"timeline"`
}
Summary is the finding of one run, and it is JSON because it ends up quoted in an evidence document and compared against a stored baseline.
func (Summary) ServerErrors ¶
ServerErrors is how many answers were a 5xx that the harness did not ask for. A shed 503 is excluded: it is the mechanism working, not the installation failing.