knit

package module
v0.1.0 Latest Latest
Warning

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

Go to latest
Published: Oct 4, 2023 License: Apache-2.0 Imports: 30 Imported by: 2

README

🧶 Knit

License Slack Build Report Card GoDoc

Knit brings GraphQL-like capabilities to RPCs. Knit has type-safe and declarative queries that shape the response, batching support to eliminate the N+1 problem, and first-class support for error handling with partial responses. It is built on top of Protobuf and Connect.

Knit is currently in alpha (α), and looking for feedback. Learn more about it at the Knit repo, and learn how to use it with the Tutorial.


This repo is an implementation in Go of the server-side of the Knit protocol. The result is a gateway that processes these declarative queries, dispatches the relevant RPCs, and then merges the results together. The actual service interface is defined in the BSR: buf.build/bufbuild/knit.

For more information on the core concepts of Knit, read the documentation in the repo that defines the protocol.

This repo contains two key components:

  1. The runtime library used by a Knit gateway: Go package "github.com/bufbuild/knit-go".
  2. A standalone server program that can be used as a Knit gateway and configured via YAML file: "github.com/bufbuild/knit-go/cmd/knitgateway.

Knit Gateway

The Knit gateway is a Go server that implements the Knit service.

The process of handling a Knit query consists of the following steps:

  1. Request Validation and Schema Computation

    The first step is to validate each requested entry point method and its associated mask. Validating the mask is done at the same time as producing the response schema, both of which involve a traversal of the mask, comparing requested field names against the RPC's response schema and the gateway's set of known relations.

  2. Issuing Entry-Point RPCs

    Once the request is validated, all indicated methods are invoked. These requests are sent concurrently (up to a configurable parallelism limit). When the gateway is configured, a route is associated with each RPC service, so this dispatch step could end up sending multiple requests to the same backend or scattering requests to many backends (depending on which methods were in the request and their configured routes).

  3. Response Masking

    Once an entry-point RPC completes, the response data is filtered according to the mask in the request. If the mask indicated any relations that must be resolved, those are accumulated in a set of "patches". A patch indicates a piece of data that must first be computed by a resolver and then inserted into the response structure.

  4. Stitching

    Stitching is the iterative process of resolving patches and adding them to the response structure. Stitching is complete when there are no patches to resolve.

    So if any patches were identified in the above step, they are aggregated into batches and sent to resolvers. Resolvers are functions that know how to compute the values of relation fields. To avoid the N+1 problem, resolvers always accept batches. All batches are resolved concurrently (up to the same configurable parallelism limit used for dispatching entry-point RPCs).

    After a resolver provides results, we go back to step 3: the result data is filtered according to the mask in the request and inserted into the response structure. If the mask for the resolved data includes more relations, a subsequent set of patches is computed, and then the gateway performs another round of stitching.

At the end of this step, the gateway has aggregated the results of all RPCs and can send a response to the client.

This process occurs for all Knit operations: Fetch, Do, and Listen. That last one is a server-stream, where the above steps are executed for each response message in the stream.

Resolvers

When services are registered, if any of the service's methods are annotated as relation resolvers, then the gateway will use that RPC method to resolve relations that appear in incoming queries.

Using the Standalone Server

This repo contains a stand-alone Knit gateway server that can get you up and going by just writing a YAML config file.

The server is a single statically-linked binary that can be downloaded from the Releases page for this repo.

You can also use the Go tool to build and install the server from source:

go install github.com/bufbuild/knit-go/cmd/knitgateway@latest

This builds a binary named knitgateway from the latest release.

Running the binary will start the server, which will by default expect a config file named knitgateway.yaml to exist in the current working directory.

The binary accepts the following command-line options:

  • --conf <filename>: Overrides the name and path of the config file to use.
  • --log-format <format>: Configures the log output format. The default is "console" format, which emits logs in a simple human-readable line-oriented text form. The other option is "json" format, which emits structured data formatted as JSON.
  • --version: Prints the version of the gateway program and then immediately exits.
Configuration

In order to configure the server, you need to provide a YAML config file. There is a an example in the root of this repo named knitgateway.example.yaml. The example file shows all the properties that can be configured. The example also is a working example if you also run the swapi-server demo server as the backend.

The YAML config format is documented in its entirety in a separate page.

Creating a Custom Gateway

You may want a custom gateway if you need the gateway to do something that the standalone gateway program does not do. This could range from custom observability or alternate logging, additional endpoints that the environment expects, add features not present in the standalone gateway (supporting other encodings, compression algorithms, protocols [e.g. HTTP/3], etc), or even embedding the gateway into the same process as a Connect or gRPC backend.

Creating a custom gateway involves writing a Go HTTP server. This server will install a handler for the Knit service, which is provided by the "github.com/bufbuild/knit-go" package in this repo.

The main steps to use this package all involve configuring the handler.

Initial Configuration

First we have to create a gateway. Note that none of the attributes are required, so it can be as simple as this:

gateway := &knit.Gateway{}

This returns a gateway that will:

  1. Use http.DefaultClient as the transport for outbound RPCs.
  2. Use Connect as the protocol (vs. gRPC or gRPC-Web) and use the Protobuf binary format as the message encoding.
  3. Have no limit on parallelism for outbound RPCs
  4. Use protoregistry.GlobalTypes for resolving extension names in requests and for resolving message names in google.protobuf.Any messages.
  5. Require that registered services include routing information (so the gateway knows where to send outbound RPCs).

You can customize the above behavior by setting various fields on the gateway:

  • Client: The transport to use for outbound RPCs. (This can also be overridden on a per-service basis, if some services require different middleware, such as auth, than others).
  • ClientOptions: The Connect client options to use for outbound RPCs. This allows customizing things like interceptors and protocols. If some backends only support gRPC, you can configure that with a client option.
  • MaxParallelismPerRequest: The concurrency limit for handling a single Knit request. Note that this controls the parallelism of issuing entry-point RPCs and the parallelism of invoking resolvers. This setting cannot be enforced inside of resolver implementations: if a resolver implementation starts other goroutines to operate with additional parallelism, this limit may be exceeded.
  • Route: This is a default route. If you have one application that will receive most (or all) of the Connect/gRPC traffic, configure it here. Then you only need to include routing information when registering services that should be routed elsewhere.
  • TypeResolver: This is an advanced option that is usually only useful or necessary when using dynamic RPC schemas. This resolver provides descriptors for extensions and messages, in case any requests or responses include extensions or google.protobuf.Any messages.

NOTE: If you want to configure a custom codec for outbound RPCs, to customize content encoding, you must use knit.WithCodec instead of connect.WithCodec when creating the Connect client option.

Configuring Entry-Point Services

Once the gateway is created with basic configuration, we register RPC services whose methods can be used as entry points for Knit operations.

The simplest way is to register services is to import the Connect generated code for these services. This generated code includes a constant for the service name and will also ensure that the relevant service descriptors are linked into your program.

package main

// This is the generated package for the Connect demo service: Eliza
import (
	"net/url"

	"buf.build/gen/go/bufbuild/eliza/bufbuild/connect-go/buf/connect/demo/eliza/v1/elizav1connect"
	"github.com/bufbuild/knit-go"
)

func main() {
	gateway := &knit.Gateway{
		Route: &url.URL{
			Scheme: "https",
			Host:   "my.backend.service:8443",
		},
		MaxParallelismPerRequest: 10,
	}
	// Refer to generated constant for service name
	err := gateway.AddServiceByName(elizav1connect.ElizaServiceName)

	// ... more configuration ...
	// ... start server ...
}

When you register a service, requests for that service will be routed to the gateway.Route URL. If that field is not set (i.e. there is no route for the service), the call to AddServiceByName will return an error.

You can supply the route (or override the default one in gateway.Route) with an option. There are other options that allow you to provide a different HTTP client and different Connect client options. These can be used if your backends are not homogenous: for example, some are Connect and some are gRPC, some support "h2c" and some do not, etc.

err := gateway.AddServiceByName(
	elizav1connect.ElizaServiceName,
	knit.WithRoute(elizaBackendURL),
	knit.WithClient(h2cClient),
	knit.WithClientOptions(connect.WithGRPC()),
	)
Starting a Server

The Knit protocol is a Protobuf service, so it can be exposed over HTTP using the Connect framework, like any other such service.

So now that our gateway is fully configured, we just wire it up as an HTTP handler:

package main

import (
	"net"
	"net/http"

	"github.com/bufbuild/knit-go"
)

// Example function for starting an HTTP server that exposes a
// configured Knit gateway.
func serveHTTP(bindAddress string, gateway *knit.Gateway) error {
	listener, err := net.Listen("tcp", bindAddress)
	if err != nil {
		return err
	}
	mux := http.NewServeMux()
	mux.Handle(path, gateway.AsHandler())
	// This returns when the server is stopped
	return http.Serve(listener, mux)
}

Now a Knit client can send requests to the HTTP server we just started.

Status: Alpha

Knit is undergoing initial development and is not yet stable.

Offered under the Apache 2 license.

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func WithCodec

func WithCodec(codec connect.Codec) connect.ClientOption

WithCodec returns a Connect client option that can be used to customize codecs used in outbound RPCs from a knit.Gateway. Do not directly use connect.WithCodec for such configuration as that will result in errors when the gateway needs to unmarshal responses from downstream Connect servers.

func WithProtoJSON

func WithProtoJSON() connect.ClientOption

WithProtoJSON returns a Connect client option that indicates that the JSON format should be used.

Types

type Gateway

type Gateway struct {
	// Client is the HTTP client to use for outbound Connect RPCs
	// for methods that do not have a custom client configured. If this
	// field is nil and a method has no custom client configured then
	// [http.Client] will be used.
	Client connect.HTTPClient
	// Route is the default base URL for routing outbound Connect RPCs.
	// It must have an "http" or "https" schema. If this field is nil.
	// nil, [WithRoute] must be used to supply routes during configuration.
	Route *url.URL
	// ClientOptions are the Connect client options used for all outbound
	// Connect RPCs. Note that use of [connect.WithCodec] is not allowed as
	// an option. To configure a custom codec, you must use [knit.WithCodec]
	// instead.
	ClientOptions []connect.ClientOption
	// TypeResolver is used to resolve extensions and types when unmarshalling
	// JSON-formatted messages. If not provided/nil, protoregistry.GlobalTypes
	// is used. This can be overridden per outbound service via a
	// [knit.WithTypeResolver] option. Even if overridden for a service,
	// this type resolver will be used to process resolved relations on
	// responses from said service.
	TypeResolver TypeResolver
	// MaxParallelismPerRequest is the maximum number of concurrent goroutines
	// that will be executing relation resolvers during the course of handling
	// a single call to the knit service. If a knit request requires more RPCs
	// than this for a single phase/depth of resolution, they will be queued.
	MaxParallelismPerRequest int
	// contains filtered or unexported fields
}

Gateway is an HTTP API gateway that accepts requests in the Knit protocol and dispatches Connect RPCs to configured backends. It is not a simple reverse gateway, like some API gateways, as it handles batching and joining of multiple RPCs, grouping the results into a single HTTP response.

Callers must use Gateway.AddService or Gateway.AddServiceByName to configure the supported services and routes. If any of the services added contain methods that resolve relations, then the gateway will support the corresponding relation in incoming Knit queries.

Once configured, the gateway is thread-safe. But configuration methods are not thread-safe. So configuration should be done from a single goroutine before sharing the instance with other goroutines, which includes registering it with an http.Server via Gateway.AsHandler.

func (*Gateway) AddService

func (g *Gateway) AddService(svc protoreflect.ServiceDescriptor, opts ...ServiceOption) error

AddService adds the given service's methods as available outbound RPCs.

func (*Gateway) AddServiceByName

func (g *Gateway) AddServiceByName(svcName protoreflect.FullName, opts ...ServiceOption) error

AddServiceByName adds the given service's methods as available outbound RPCs. This function uses protoregistry.GlobalFiles to resolve the name. So the given service name must be defined in a file whose corresponding generated Go code is linked into the current program.

func (*Gateway) AsHandler

func (g *Gateway) AsHandler(handlerOptions ...connect.HandlerOption) (path string, h http.Handler)

AsHandler returns an HTTP handler for g as well as the URI path that the handler expects to handle. The returned values can be passed directly to an *http.ServeMux's Handle method:

mux.Handle(gateway.AsHandler())

type ServiceOption

type ServiceOption func(*svcOpts)

ServiceOption is an option for configuring an outbound service.

func WithClient

func WithClient(client connect.HTTPClient) ServiceOption

WithClient is a service option that indicates the HTTP client that should be used when routing RPCs for a particular service.

func WithClientOptions

func WithClientOptions(opts ...connect.ClientOption) ServiceOption

WithClientOptions is a service option that indicates the Connect client options that should be used when routing RPCs for a particular service.

Note: connect.WithCodec should not be used directly and provided to this function. Instead, use knit.WithCodec to configure custom codecs.

func WithRoute

func WithRoute(baseURL *url.URL) ServiceOption

WithRoute is a service option that indicates the base URL that should be used when routing RPCs for a particular service.

func WithTypeResolver

func WithTypeResolver(res TypeResolver) ServiceOption

WithTypeResolver is a service option that indicates a resolver to use to resolve extensions and the contents of google.protobuf.Any messages when serializing and de-serializing requests and responses.

type TypeResolver

TypeResolver is capable of resolving messages and extensions. This is needed for JSON serialization.

Jump to

Keyboard shortcuts

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