Awesome
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!