Complexity control
GraphQL queries offer lots of flexiblity to build the queries you need. But this flexibility can be abused, causing excess load on downstream servers.
Operation limits allow users to set a high water mark on many of the properties of a GraphQL query. But they are quite a blunt tool - not all subgraphs have the same performance characteristics and even within a subgraph not all fields neccesarily cause the same load.
That's where complexity control comes in: it allows you to set an overall complexity limit in the Grafbase Gateway, but leaves the definition of how complex each field is up to the developers of the subgraphs.
This can be enabled in the new [operation_limits]
section of the gateway
configuration:
[complexity_control]
mode = "enforce"
limit = 10000
list_size = 100
The available options here are:
mode
is the setting that enables complexity control. It can be set to one of two values:enforce
will calculate the complexity of all queries and reject incoming requests that exceed the configured limit.measure
will calculate the complexity of all queries but only report the complexity
limit
sets the complexity limit for incoming queries (when mode isenforce
).list_size
sets the default assumed size of lists in queries when their associated field does not have a@listSize
directive.
It's up to each individual subgraph to define the compleixty of it's fields.
This can be acheived with two directives: @cost
& @listSize
The @cost
directive is defined as such:
directive @cost(
weight: Int!
) on ARGUMENT_DEFINITION | ENUM | FIELD_DEFINITION | INPUT_FIELD_DEFINITION | OBJECT | SCALAR
This directive can be provided on a field, argument, or type. When provided on a field or argument it sets the cost of that field or argument appearing in a query. When provided on a type it sets the cost of a field of that type appearing in a query. If an individual field has a cost then that will be override any cost set on the type of that field.
If no cost directive can be found for a particular field or it's type, then a default cost will be applied. If the field is of a scalar type, then its cost is assumed to be zero. If the field is of an object type, then its default cost is 1.
The @listSize
directive is defined as such:
directive @listSize(
assumedSize: Int
slicingArguments: [String!]
sizedFields: [String!]
requireOneSlicingArgument: Boolean = true
) on FIELD_DEFINITION
This directive controls the size that we assume each list field has. In brief it's arguments are:
assumedSize
if provided sets the size that we assume this list is.slicingArguments
says that the given arguments to this field define the length of the list.sizedFields
can be used on connection fields that are following the GraphQL cursor specification to indicate which subfields of the current field are controlled by the slicing arguments on this field.requireOneSlicingArgument
can be set when slicing arguments is also set. If set an error will be raised if we receive a query for this field that doesn't have exactly one slicing argument provided. This argument defaults to true, but if slicingArguments is not provided it is not used.
For more details you can read the detailed specification of @listSize
in the
Cost Directive Specification.
The complexity score of an operation is calculated by walking the query, and summing up the cost of each individual field. Fields are assigned a cost according to the cost of the field or the type of the field, plus the cost of all their children. If the field in question is a list then its cost is multiplied by the expected size of the list.
For example this query would be calculated as:
query {
# (self + children) * listSize = (1 + 1) * 4 = 8
products(limit: 4) {
id # scalar: 0
title # scalar: 0
price # scalar: 0
author {
# object: 1
id # scalar: 0
name # scalar: 0
}
}
}