hardy

package module
v0.2.0 Latest Latest
Warning

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

Go to latest
Published: Feb 9, 2023 License: MIT Imports: 12 Imported by: 0

README

codecov

Hardy

Hardy is wrapper around http.Client that enables you to add more resilience and reliability for your HTTP calls through retries. As retry strategy, it uses the exponential backoff algorithm and jitter to ensure that our services doesn't cause total outage to their dependencies. Besides that, it also provides some useful features like debug mode and default User-Agent headers.

Usage

Install

go get github.com/diegohordi/hardy

Creating the client

Hardy client already creates a http.Client by default, but you can override it, and configure it the way you want it, as follows:

Optional parameters:

  • WithHttpClient - will use the given http.Client to perform the requests.
  • WithDebugger - will use the given debugger to print out the debug output.
  • WithDebugDisabled - will disable the debug mode, which is enabled by default.
  • WithNoUserAgentHeader - will use not User-Agent header.
  • WithUserAgentHeader - will use a custom User-Agent header.
  • WithMaxRetries - will determine how many retries should be attempted.
  • WithWaitInterval - will define the base duration between each retry.
  • WithMultiplier - the multiplier that should be used to calculate the backoff interval. Should be greater than the hardy.DefaultMultiplier.
  • WithMaxInterval - the max interval between each retry. If no one was given, the interval between each retry will grow exponentially.
httpClient := &http.Client{Timeout: 3 * time.Second}
client, err := hardy.NewClient(
    WithHttpClient(httpClient),
    WithMaxRetries(4),
	WithWaitInterval(3 * time.Millisecond),
	WithMultiplier(hardy.DefaultMultiplier),
	WithMaxInterval(3 * time.Second), 
)		   
Using the client

The wrapper adds the method Try(context.Context, *http.Request, hardy.ReaderFunc, hardy.FallbackFunc), which receives:

  • context.Context a proper context to the request, mandatory. Hardy is also enabled to deal with context deadline/cancellation. -*http.Request an instance of the request that should be performed, mandatory.
  • hardy.ReaderFunc a reader function, mandatory, that will be responsible to handle each request result. -hardy.FallbackFunc a fallback function that will be called if all retries fail, optional.
hardy.ReaderFunc

The ReaderFunc defines the function responsible to read the HTTP response and also determines if a new retry must be performed returning an error or not, returning nil.

Keep in mind while writing your reader function that we shouldn't perform a retry if the response contains an error due to a client error (400-499 HTTP error codes), but consider only the ones not caused by them instead, as 500 and 503 HTTP error codes, for instance.

Example

// MessageService is a service used to call HTTP Bin API
type MessageService struct {
    Client *hardy.Client
}

// Message is the message that will be sent
type Message struct {
    Message string `json:"message"`
}

// PostMessageWithFallback sends a message with some possible HTTP status code responses, comma separated.
func (s *MessageService) PostMessage(ctx context.Context, message Message) (string, error) {
    
    // Marshal the given message to send to API
    b, err := json.Marshal(&message)
    if err != nil {
        return "", err
    }

    // Create a new API request
    request, err := http.NewRequestWithContext(ctx, http.MethodPost, "http://someapi", bytes.NewReader(b))
    if err != nil {
        return "", err
    }

    // helloMessage will hold the message returned by the API
    var helloMessage string
    var reqErr error

    readerFunc := func(response *http.Response) error {
        if response.StatusCode == http.StatusBadRequest {
            reqErr = fmt.Errorf("error while posting message: %d - %s", response.StatusCode, response.Status)
            return nil
        }
        if response.StatusCode >= 500 {
            return fmt.Errorf(response.Status) // Will retry    	
        }
        var responseStruct ResponseMessageStruct
        if err := json.NewDecoder(response.Body).Decode(&responseStruct); err != nil {
            reqErr = fmt.Errorf("error while parsing response body: %w", err)
            return nil
        }
        helloMessage = responseStruct.Message
        return nil
    }

    // Create a fallback function with the API doesn't respond properly
	fallbackFunc := func() error {
        helloMessage = fmt.Sprintf("Hello from fallback!")
        return nil
    }

    // Try to execute the request
	err = s.Client.Try(ctx, request, readerFunc, fallbackFunc)
    if err != nil {
        return "", err
	}

    if reqErr != nil {
        return "", reqErr
    }

    return helloMessage, nil
}

// Create the Hardy client
client, err := hardy.NewClient(
    hardy.WithDebugDisabled(),
    hardy.WithMaxRetries(3),
    hardy.WithWaitInterval(3*time.Millisecond),
    hardy.WithMaxInterval(3*time.Second),
)
if err != nil {
    panic(err)
}

// Create the message service
messageService := &MessageService{
    Client: client,
}

// Post the message
message, err := messageService.PostMessage(context.Background(), Message{Message: "Hello John Doe"})
if err != nil {
    panic(err)
}

Tests

The coverage so far is greater than 90%, covering also failure scenarios, and also, there are no race conditions detected in the -race tests. For integration tests, it uses HTTP BIN.

You can run the tests from Makefile, as below:

Unit tests

make tests

Integration tests

make integration_tests

Documentation

Overview

Package hardy contains a wrapper for http.Client with some extra features, like common headers, debugger, fallback and exponential backoff as retry mechanism.

Index

Constants

View Source
const (

	// ClientVersion is the client version
	ClientVersion = "0.2.0"

	// DefaultWaitIntervalMilliseconds is the default wait interval in milliseconds between each retry.
	DefaultWaitIntervalMilliseconds = 500

	// DefaultMaxIntervalInMilliseconds is the default maximum wait interval in milliseconds between each retry.
	DefaultMaxIntervalInMilliseconds = 5000

	// DefaultMaxRetries is the default maximum allowed retries.
	DefaultMaxRetries = 3

	// DefaultBackoffMultiplier is the default backoff multiplier used to get next intervals.
	DefaultBackoffMultiplier = 2

	// DefaultTimeoutInSeconds is the maximum timeout for each attempt in seconds.
	DefaultTimeoutInSeconds = 10
)

Variables

This section is empty.

Functions

This section is empty.

Types

type Client

type Client struct {
	// contains filtered or unexported fields
}

func NewClient

func NewClient(options ...Option) (*Client, error)

NewClient creates a new Hardy wrapper with the defaults or an error if it was misconfigured by some given option.

func (*Client) Try

func (c *Client) Try(ctx context.Context, req *http.Request, readerFunc ReaderFunc, fallbackFunc FallbackFunc) error

Try tries to perform the given request as per configurations. If some FallbackFunc is given, after max retries were reached, it will be called. It might return the following errors:

- ErrNoReaderFuncFound - when no reader function was provided.

- ErrMaxRetriesReached - if max retries were reached.

- context.DeadlineExceeded or context.Canceled - if the given context was gone.

- ErrUnexpected is the error returned when no one of the previous errors match.

type Debugger added in v0.2.0

type Debugger interface {
	Println(v ...any)
}

Debugger declares the methods that the debuggers should implement.

type Error

type Error struct {

	// ErrorCode is a well-known error code.
	ErrorCode ErrorCode `json:"error_code"`

	// HTTPStatusCode is the equivalent HTTP status code.
	HTTPStatusCode int `json:"status_code"`

	// Message is the user-friendly error message.
	Message string `json:"message"`
	// contains filtered or unexported fields
}

Error represents the structured errors returned by the client.

func (Error) Error

func (e Error) Error() string

Error returns the string representation of the given error as JSON.

func (Error) Is added in v0.2.0

func (e Error) Is(tgt error) bool

Is checks if the given target error equals to this error code

type ErrorCode added in v0.2.0

type ErrorCode string

ErrorCode is the type of well-known error codes.

const (

	// ErrInvalidClientConfiguration is the error returned when some client configuration is invalid.
	ErrInvalidClientConfiguration ErrorCode = "invalid_configuration_error"

	// ErrNoDebuggerFound is the error returned when the debug mode was enabled but debugger was given.
	ErrNoDebuggerFound ErrorCode = "no_debugger_found_error"

	// ErrNoHTTPClientFound is the error returned when no HTTP Client was given.
	ErrNoHTTPClientFound ErrorCode = "no_http_client_found_error"

	// ErrNoReaderFuncFound is the error returned when no ReaderFunc was given.
	ErrNoReaderFuncFound ErrorCode = "no_reader_func_found_error"

	// ErrMaxRetriesReached is the error returned when the max allowed retries were reached.
	ErrMaxRetriesReached ErrorCode = "max_retries_reached_error"

	// ErrUnexpected is the error returned when no one of the previous errors match.
	ErrUnexpected ErrorCode = "unexpected_error"
)

func (ErrorCode) Error added in v0.2.0

func (e ErrorCode) Error() string

Error returns the string representation of the given error.

type FallbackFunc

type FallbackFunc func() error

FallbackFunc defines the function that should be used as fallback when max retries was reached out.

type Option added in v0.2.0

type Option func(c *Client) error

Option defines the optional configurations for the Client.

func WithBackoffMultiplier added in v0.2.0

func WithBackoffMultiplier(multiplier float64) Option

WithBackoffMultiplier Determines the multiplier that should be used to calculate the backoff interval.

func WithDebugDisabled added in v0.2.0

func WithDebugDisabled() Option

WithDebugDisabled disables the debug mode.

func WithDebugger added in v0.2.0

func WithDebugger(debugger Debugger) Option

WithDebugger enables the debug mode, dumping the requests to output using the client logger.

func WithHttpClient added in v0.2.0

func WithHttpClient(httpClient *http.Client) Option

WithHttpClient overrides the default HTTP Client used by the one given.

func WithMaxInterval added in v0.2.0

func WithMaxInterval(interval time.Duration) Option

WithMaxInterval determines the max interval between each fail request.

func WithMaxRetries added in v0.2.0

func WithMaxRetries(maxRetries int) Option

WithMaxRetries determines how many retries should be attempted.

func WithNoUserAgentHeader added in v0.2.0

func WithNoUserAgentHeader() Option

WithNoUserAgentHeader disables adding the User-Agent header in the request.

func WithUserAgentHeader added in v0.2.0

func WithUserAgentHeader(userAgent string) Option

WithUserAgentHeader enables adding the User-Agent header in the request and overrides the default one.

func WithWaitInterval added in v0.2.0

func WithWaitInterval(interval time.Duration) Option

WithWaitInterval determines the base duration between each fail request.

type ReaderFunc

type ReaderFunc func(response *http.Response) error

ReaderFunc defines the function responsible to read the HTTP response and also determines if a new retry must be performed returning an error or not, returning nil.

Keep in mind while writing your reader function that we shouldn't perform a retry if the response contains an error due to a client error (400-499 HTTP error codes), but consider only the ones not caused by them instead, as 500 and 503 HTTP error codes, for instance.

Jump to

Keyboard shortcuts

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