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
- func BuildWSFrame(spec *ir.WSSpec, resolve Resolver) ([]byte, error)
- func GRPCCodeName(c codes.Code) string
- type Frame
- type GRPCCall
- type GRPCConns
- type GRPCMethod
- type GRPCResponse
- type GraphQLResult
- type ProtoRegistry
- type Request
- func BuildGRPCRequest(spec *ir.GRPCSpec, resolve Resolver) (*Request, error)
- func BuildGraphQLRequest(spec *ir.GraphQLSpec, resolve Resolver) (*Request, error)
- func BuildRequest(spec *ir.CallSpec, resolve Resolver) (*Request, error)
- func BuildWSOpen(spec *ir.WSSpec, resolve Resolver) (*Request, error)
- type Resolver
- type Response
- type Session
- type SessionOptions
- type WSCloseError
- type WSSession
Constants ¶
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.
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.
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 ¶
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 ¶
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 ¶
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 (*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.
type Request ¶
type Request struct {
Method string
URL string
Headers map[string]string
Query map[string]string
Body []byte
}
func BuildGRPCRequest ¶
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 ¶
BuildRequest expands templates; body values are JSON-escaped so a quote in fixture data cannot corrupt the document.
func BuildWSOpen ¶
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 ¶
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 ¶
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.
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.
type SessionOptions ¶
type WSCloseError ¶
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 ¶
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.