/circuitgen

Primary LanguageGoApache License 2.0Apache-2.0

circuitgen

circuitgen generates a circuit wrapper around an interface or struct that encapsulates calling circuits. A wrapper struct matching the interface or struct method set is generated, and each method call that is context-aware and returning an error is wrapped by a circuit. These wrapper structs have no outside Go dependencies besides the interface or struct's dependencies.

It's important to provide a IsBadRequest to not count user errors against the circuit. A bad request is not counted as a success or failure in the circuit, so it does not affect opening or closing the circuit. For example, a spike in HTTP 4xx errors (ex. Validation errors) should not open the circuit.

An optional ShouldSkipError can be provided so that the call is counted as successful even if there is a non-nil error. For example, DynamoDB responses that return ConditionalCheckedFailException (CCFE) should be counted as successful requests. In the scenario where CCFE is counted as a bad request, if the client is getting CCFE a majority of the time, and the circuit opens (ex. spike of timeouts), then the circuit will prolong closing the circuit until the circuit happens to make a request that doesn't return CCFE.

Method Wrapping Requirements

When deciding if making a circuit wrapper is right for your interface or struct, consider that methods will only be wrapped if:

  • The method accepts a context as the first argument
  • The method returns an error as the last value

Example

type Publisher interface {
	// Method is wrapped
	Publish(ctx context.Context, message string) error
	// Method is *not* wrapped
	Close() error
}

Installation

go get github.com/twitchtv/circuitgen

Usage

circuitgen --pkg <package path> --name <type name> --out <output path> [--alias <alias>] [--circuit-major-version <circuit major version>]

Add ./vendor/ to package path if the dependency is vendored; when using Go modules this is unnecessary.

Set the circuit-major-version flag if using Go modules and major version 3 or later. This makes the wrappers import the same version as the rest of your code.

Example

Generating the DynamoDB client into the wrappers directory with circuits aliased as "DynamoDB"

circuitgen --pkg github.com/aws/aws-sdk-go/service/dynamodb/dynamodbiface --name DynamoDBAPI --alias DynamoDB --out internal/wrappers --circuit-major-version 3

This generates a circuit wrapper that satifies the github.com/aws/aws-sdk-go/service/dynamodb/dynamodbiface.DynamoDBAPI interface.

// Code generated by circuitgen tool. DO NOT EDIT

package wrappers

import (
	"context"

	"github.com/aws/aws-sdk-go/aws/request"
	"github.com/aws/aws-sdk-go/service/dynamodb"
	"github.com/aws/aws-sdk-go/service/dynamodb/dynamodbiface"
	"github.com/cep21/circuit/v3"
)

// CircuitWrapperDynamoDBConfig contains configuration for CircuitWrapperDynamoDB. All fields are optional
type CircuitWrapperDynamoDBConfig struct {
	// ShouldSkipError determines whether an error should be skipped and have the circuit
	// track the call as successful. This takes precedence over IsBadRequest
	ShouldSkipError func(error) bool

	// IsBadRequest is an optional bad request checker. It is useful to not count user errors as faults
	IsBadRequest func(error) bool

	// Prefix is prepended to all circuit names
	Prefix string

	// Defaults are used for all created circuits. Per-circuit configs override this
	Defaults circuit.Config

	// CircuitBatchGetItemPagesWithContext is the configuration used for the BatchGetItemPagesWithContext circuit. This overrides values set by Defaults
	CircuitBatchGetItemPagesWithContext circuit.Config
	// CircuitBatchGetItemWithContext is the configuration used for the BatchGetItemWithContext circuit. This overrides values set by Defaults
	CircuitBatchGetItemWithContext circuit.Config

	// ... Rest omitted
}

// CircuitWrapperDynamoDB is a circuit wrapper for dynamodbiface.DynamoDBAPI
type CircuitWrapperDynamoDB struct {
	dynamodbiface.DynamoDBAPI

	// ShouldSkipError determines whether an error should be skipped and have the circuit
	// track the call as successful. This takes precedence over IsBadRequest
	ShouldSkipError func(error) bool

	// IsBadRequest checks whether to count a user error against the circuit. It is recommended to set this
	IsBadRequest func(error) bool

	// CircuitBatchGetItemPagesWithContext is the circuit for method BatchGetItemPagesWithContext
	CircuitBatchGetItemPagesWithContext *circuit.Circuit
	// CircuitBatchGetItemWithContext is the circuit for method BatchGetItemWithContext
	CircuitBatchGetItemWithContext *circuit.Circuit

	// ... Rest omitted
}

// NewCircuitWrapperDynamoDB creates a new circuit wrapper and initializes circuits
func NewCircuitWrapperDynamoDB(
	manager *circuit.Manager,
	embedded dynamodbiface.DynamoDBAPI,
	conf CircuitWrapperDynamoDBConfig,
) (*CircuitWrapperDynamoDB, error) {
	if conf.ShouldSkipError == nil {
		conf.ShouldSkipError = func(err error) bool {
			return false
		}
	}

	if conf.IsBadRequest == nil {
		conf.IsBadRequest = func(err error) bool {
			return false
		}
	}

	w := &CircuitWrapperDynamoDB{
		DynamoDBAPI:     embedded,
		ShouldSkipError: conf.ShouldSkipError,
		IsBadRequest:    conf.IsBadRequest,
	}

	var err error

	w.CircuitBatchGetItemPagesWithContext, err = manager.CreateCircuit(conf.Prefix+"DynamoDB.BatchGetItemPagesWithContext", conf.CircuitBatchGetItemPagesWithContext, conf.Defaults)
	if err != nil {
		return nil, err
	}

	w.CircuitBatchGetItemWithContext, err = manager.CreateCircuit(conf.Prefix+"DynamoDB.BatchGetItemWithContext", conf.CircuitBatchGetItemWithContext, conf.Defaults)
	if err != nil {
		return nil, err
	}

	// ... Rest omitted

	return w, nil
}

// BatchGetItemPagesWithContext calls the embedded dynamodbiface.DynamoDBAPI's method BatchGetItemPagesWithContext with CircuitBatchGetItemPagesWithContext
func (w *CircuitWrapperDynamoDB) BatchGetItemPagesWithContext(ctx context.Context, p1 *dynamodb.BatchGetItemInput, p2 func(*dynamodb.BatchGetItemOutput, bool) bool, p3 ...request.Option) error {
	var skippedErr error

	err := w.CircuitBatchGetItemPagesWithContext.Run(ctx, func(ctx context.Context) error {
		err := w.DynamoDBAPI.BatchGetItemPagesWithContext(ctx, p1, p2, p3...)

		if w.ShouldSkipError(err) {
			skippedErr = err
			return nil
		}

		if w.IsBadRequest(err) {
			return &circuit.SimpleBadRequest{Err: err}
		}

		return err
	})

	if skippedErr != nil {
		err = skippedErr
	}

	if berr, ok := err.(*circuit.SimpleBadRequest); ok {
		err = berr.Err
	}

	return err
}

// BatchGetItemWithContext calls the embedded dynamodbiface.DynamoDBAPI's method BatchGetItemWithContext with CircuitBatchGetItemWithContext
func (w *CircuitWrapperDynamoDB) BatchGetItemWithContext(ctx context.Context, p1 *dynamodb.BatchGetItemInput, p2 ...request.Option) (*dynamodb.BatchGetItemOutput, error) {
	var r0 *dynamodb.BatchGetItemOutput
	var skippedErr error

	err := w.CircuitBatchGetItemWithContext.Run(ctx, func(ctx context.Context) error {
		var err error
		r0, err = w.DynamoDBAPI.BatchGetItemWithContext(ctx, p1, p2...)

		if w.ShouldSkipError(err) {
			skippedErr = err
			return nil
		}

		if w.IsBadRequest(err) {
			return &circuit.SimpleBadRequest{Err: err}
		}

		return err
	})

	if skippedErr != nil {
		err = skippedErr
	}

	if berr, ok := err.(*circuit.SimpleBadRequest); ok {
		err = berr.Err
	}

	return r0, err
}

// ... Rest of methods omitted

var _ dynamodbiface.DynamoDBAPI = (*CircuitWrapperDynamoDB)(nil)

The wrapper can be used like such

func createWrappedClient() (dynamodbiface.DynamoDBAPI, error) {
	m := &circuit.Manager{} // Simplest manager

	// Create embedded client
	sess := session.Must(session.NewSession(&aws.Config{}))
	client := dynamodb.New(sess)

	// Create circuit wrapped client
	wrappedClient, err := wrappers.NewCircuitWrapperDynamoDB(m, client, wrappers.CircuitWrapperDynamoDBConfig{
		// Custom check to skip errors to not count against the circuit. For DynamoDB specifically, ConditionalCheckFailedException
		// errors are considered successful requests
		ShouldSkipError: func(err error) bool {
			aerr, ok := err.(awserr.Error)
			return ok && aerr.Code() == dynamodb.ErrCodeConditionalCheckFailedException
		},
		// Custom check for bad request. This is important to not count user errors as faults.
		// See https://github.com/cep21/circuit#not-counting-user-error-as-a-fault
		IsBadRequest: func(err error) bool {
			rerr, ok := err.(awserr.RequestFailure)
			if ok {
				return rerr.StatusCode() >= 400 && rerr.StatusCode() < 500
			}
			return false
		},
		// Override defaults for the GetItemWithContext circuit
		CircuitGetItemWithContext: circuit.Config{
			Execution: circuit.ExecutionConfig{
				Timeout: 200 * time.Millisecond, // Override default timeout
			},
		},
	})
	if err != nil {
		return nil, err
	}

	return wrappedClient, nil
}

Development

Go version 1.12 or beyond is recommended for development.

Run make test to run Go tests.

License

This library is licensed under the Apache 2.0 License.

Contributing

Any pull requests are extremely welcome! If you run into problems or have questions, please raise a github issue!