adapters

package
v0.1.1 Latest Latest
Warning

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

Go to latest
Published: Jul 30, 2026 License: Apache-2.0 Imports: 33 Imported by: 0

Documentation

Overview

Package adapters implements protocol adapters (HTTP first) behind a common step interface, each emitting per-phase spans and classifying the outcome.

Index

Constants

View Source
const DefaultReceiveTimeout = 5 * time.Second

DefaultReceiveTimeout bounds a receive whose step declares none. Without a bound a frame that never arrives holds the VU until the run is cancelled, which reads as a hang rather than as the failed assertion it is.

View Source
const StatusTryAgainLater = int(websocket.StatusTryAgainLater)

StatusTryAgainLater is RFC 6455's 1013: the peer is ending the connection because it is overloaded, and would like to be asked again later. It is the in-band `429` — the only close code that classifies as throttled rather than failed.

View Source
const SupportedProtoSyntax = "proto2, proto3, and edition 2023"

SupportedProtoSyntax is what the bundled compiler accepts. It is a property of the pinned protocompile version, not of FlowBench, and it moves when that dependency does — which is exactly why a flow gets told rather than left to read a parse error about a keyword the compiler has never heard of.

Variables

This section is empty.

Functions

func BuildWSFrame

func BuildWSFrame(spec *ir.WSSpec, resolve Resolver) ([]byte, error)

BuildWSFrame templates the frame the step sends. Values are JSON-escaped by the same resolver call bodies use, so an extracted value carrying a quote cannot rewrite the message.

func GRPCCodeName

func GRPCCodeName(c codes.Code) string

GRPCCodeName renders a status in gRPC's canonical vocabulary — RESOURCE_EXHAUSTED rather than grpc-go's CamelCase — because that is the spelling in the spec, in grpcurl, and in the service's own .proto comments.

Types

type Frame

type Frame struct {
	Payload []byte
	Binary  bool
}

Frame is one received message. Binary frames arrive with their payload intact and Binary set: matching and extraction read JSON, so a binary frame simply matches nothing, which is the honest outcome rather than an error the author cannot act on.

type GRPCCall

type GRPCCall struct {
	// Method is the resolved schema, from the run's ProtoRegistry.
	Method *GRPCMethod

	// Request holds the address (as the URL), the JSON message (as the body),
	// and the metadata (as the headers). It is an ordinary Request because
	// everything that decorates one should reach a gRPC call unchanged.
	Request *Request
}

GRPCCall is one resolved unary call: where it goes, which method it invokes, and the request that carries it.

type GRPCConns

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

GRPCConns is one VU's gRPC channels, one per address.

Per VU rather than per run, matching the dedicated transport each HTTP session gets: a channel multiplexes every call a VU makes over one HTTP/2 connection, and sharing one across 10k VUs would measure that channel's stream limit rather than the target.

func NewGRPCConns

func NewGRPCConns(timeout time.Duration) *GRPCConns

func (*GRPCConns) Close

func (c *GRPCConns) Close()

Close releases every channel. Teardown, not measurement: it runs when the VU ends, however it ended.

func (*GRPCConns) Invoke

func (c *GRPCConns) Invoke(ctx context.Context, stepID string, call *GRPCCall, anchor time.Time) (*GRPCResponse, *span.Span, error)

Invoke performs one unary call. anchor is the iteration start; all span offsets are relative to it, and the span tree comes back even on failure.

A non-OK status is a response, not an error — the status is data the flow's own assertions judge, exactly as an HTTP 500 is. The exception is the set of codes that mean the call never reached an application handler at all; those come back as an error, because a target that is not there is a failed call and not a target with an opinion.

type GRPCMethod

type GRPCMethod struct {
	// Path is the fully-qualified method with its leading slash — literally the
	// HTTP/2 `:path` gRPC puts on the wire.
	Path string

	Input  protoreflect.MessageDescriptor
	Output protoreflect.MessageDescriptor

	// Resolver interprets Any and extensions inside those messages. Without it
	// protojson renders a `google.protobuf.Any` as an error rather than as the
	// message it wraps.
	Resolver linker.Resolver
}

GRPCMethod is one resolved unary method: the two message types it moves and the resolver that can interpret anything they reference.

type GRPCResponse

type GRPCResponse struct {
	// Code is the numeric gRPC status — 0 is OK. It is what `status ==`
	// assertions, `throttle.statuses` and `retry.on_status` compare against,
	// because those are ints everywhere else in the IR too.
	Code codes.Code

	// Message is the status message, empty on OK.
	Message string

	// Body is the response message rendered as JSON, so JSONPath extraction and
	// body assertions work on it unchanged. A non-OK call carries no message,
	// so this is nil and the status is the whole answer.
	Body []byte

	// Headers are the response metadata, headers and trailers together, as
	// http.Header so `header.x` reads the same as it does on an HTTP step.
	Headers http.Header

	// RetryAfter is the server asking to be left alone, if it said so.
	RetryAfter string
}

GRPCResponse is what a unary call answered: the status in both forms the rest of the engine needs, the response message as JSON, and the metadata.

func (*GRPCResponse) StatusText

func (r *GRPCResponse) StatusText() string

StatusText is the status in gRPC's own vocabulary — RESOURCE_EXHAUSTED, not the number 8 and emphatically not "HTTP 8".

type GraphQLResult

type GraphQLResult struct {
	// Errors are the operation's own errors, already flattened to their
	// messages. Empty means the operation reported none.
	Errors []string
	// HasData reports whether `data` came back non-null. A response carrying
	// both data and errors is a partial success, which federated graphs return
	// routinely when one subgraph fails and the rest resolve.
	HasData bool
	// Malformed is set when the body is not a GraphQL response at all — an
	// HTML error page from a proxy, say. Callers treat it as a failure whatever
	// the error policy says, since nothing can be extracted from it.
	Malformed error
}

GraphQLResult is what a response says about the operation, as opposed to what the transport says about the request.

func ReadGraphQLResult

func ReadGraphQLResult(body []byte) GraphQLResult

ReadGraphQLResult inspects a response body for the data/errors shape.

type ProtoRegistry

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

ProtoRegistry compiles .proto files to descriptors and hands out methods.

Compilation happens once per file per run, not once per call: parsing and linking a schema is real work, and at 10k VUs doing it per iteration would measure the generator rather than the target. The zero benefit of laziness here is why Prepare exists — a broken proto or a method that is not in it is a pre-run error, in the same class as an unreachable host, rather than a failure that only shows up once the run is under way.

func NewProtoRegistry

func NewProtoRegistry(root string) *ProtoRegistry

func (*ProtoRegistry) Method

func (r *ProtoRegistry) Method(ctx context.Context, spec *ir.GRPCSpec) (*GRPCMethod, error)

Method resolves the spec's method, compiling its proto on first use.

func (*ProtoRegistry) Prepare

func (r *ProtoRegistry) Prepare(ctx context.Context, sc *ir.Scenario) error

Prepare resolves every gRPC method in the scenario, so a schema problem is reported before the first request rather than by every VU at once.

type Request

type Request struct {
	Method  string
	URL     string
	Headers map[string]string
	Query   map[string]string
	Body    []byte
}

func BuildGRPCRequest

func BuildGRPCRequest(spec *ir.GRPCSpec, resolve Resolver) (*Request, error)

BuildGRPCRequest turns a step into the request that carries it. The message is templated with the JSON-escaping resolver call bodies use, so a quote in an extracted value cannot corrupt the document it is about to become.

The URL is left as the step wrote it; the executor resolves it against the target's base address and appends the method, since the method is the path.

func BuildGraphQLRequest

func BuildGraphQLRequest(spec *ir.GraphQLSpec, resolve Resolver) (*Request, error)

BuildGraphQLRequest turns an operation into the HTTP request that carries it. Variables are templated with the JSON-escaping resolver, the same one call bodies use, so a quote or newline in an extracted value cannot break out of the document.

func BuildRequest

func BuildRequest(spec *ir.CallSpec, resolve Resolver) (*Request, error)

BuildRequest expands templates; body values are JSON-escaped so a quote in fixture data cannot corrupt the document.

func BuildWSOpen

func BuildWSOpen(spec *ir.WSSpec, resolve Resolver) (*Request, error)

BuildWSOpen turns an opening step into the handshake request. It is an ordinary GET, which is the point: everything that decorates a request — templating, auth, the allow-list — applies to it unchanged.

func (*Request) AddCookie

func (r *Request) AddCookie(name, value string)

AddCookie sets one cookie in the Cookie header, keeping the others and replacing any pair of the same name. Keeping the others is why a declared cookie rides alongside the ones the step's own headers carry; replacing by name is why re-applying on a retry does not send it twice.

func (*Request) FinalURL

func (r *Request) FinalURL() (*url.URL, error)

FinalURL is the request as it will be sent: URL with Query merged in. The transport and the HMAC signer both build it here, so a signature always covers the URL that actually goes on the wire.

func (*Request) SetHeader

func (r *Request) SetHeader(name, value string)

SetHeader sets a header, allocating the map on first use.

func (*Request) SetQuery

func (r *Request) SetQuery(name, value string)

SetQuery sets a query parameter, allocating the map on first use.

type Resolver

type Resolver func(ref string) (string, error)

type Response

type Response struct {
	Status  int
	Headers http.Header
	Body    []byte
}

type Session

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

func NewSession

func NewSession(opts SessionOptions) *Session

func (*Session) DialWS

func (s *Session) DialWS(ctx context.Context, spanName string, req *Request, subprotocols []string, anchor time.Time) (*WSSession, *Response, *span.Span, error)

DialWS opens a session. The Request carries the URL and whatever headers the step and its auth put on the handshake, so it is built and decorated exactly like a call step's request; the *Response is the handshake response, present even when the upgrade was refused so the caller can classify the status.

The returned span mirrors Session.Do's: an `http_call` child with the dns/connect/tls/ttfb phases under it, because the handshake really is one.

func (*Session) Do

func (s *Session) Do(ctx context.Context, stepID string, req *Request, anchor time.Time) (*Response, *span.Span, error)

Do executes one call step. anchor is the iteration start; all span offsets are relative to it. The span tree is returned even on failure.

type SessionOptions

type SessionOptions struct {
	Timeout time.Duration
}

type WSCloseError

type WSCloseError struct {
	Code   int
	Reason string
}

WSCloseError is the peer ending the session, as opposed to the transport failing. Code is the RFC 6455 close code.

func AsWSCloseError

func AsWSCloseError(err error) (*WSCloseError, bool)

AsWSCloseError reports whether err is the peer closing the session, and with which code. A close is not a transport failure — it is the far end's verdict on the conversation, and 1013 in particular is a throttle.

func (*WSCloseError) Error

func (e *WSCloseError) Error() string

type WSSession

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

WSSession is one open WebSocket connection, held for the life of an iteration. It is the seam the library sits behind.

func (*WSSession) Close

func (s *WSSession) Close()

Close ends the session with a normal closure. Best effort: a peer that has already gone leaves nothing to say goodbye to, and CloseNow releases the connection regardless.

func (*WSSession) Receive

func (s *WSSession) Receive(ctx context.Context, timeout time.Duration) (Frame, error)

Receive reads the next frame, waiting at most timeout. A zero timeout uses the default.

The deadline is per receive rather than per frame on purpose: a step waiting for one frame among many should time out when it has waited long enough in total, not be kept alive indefinitely by a heartbeat it is skipping.

func (*WSSession) Send

func (s *WSSession) Send(ctx context.Context, payload []byte) error

Send writes one text frame. v0 speaks JSON, so the payload is the frame.

func (*WSSession) URL

func (s *WSSession) URL() string

URL is the resolved address the session was opened against.

Jump to

Keyboard shortcuts

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