goretry

package module
v0.0.1 Latest Latest
Warning

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

Go to latest
Published: Oct 7, 2025 License: MIT Imports: 7 Imported by: 0

README

GoRetry

A reusable Go package implementing a process retry pattern with transient exception strategy, inspired by my System.Retry C# library.

Features

  • Flexible Retry Policies: Support for exponential backoff, linear backoff, fixed delay, and custom policies
  • Transient Error Detection: Configurable logic to determine which errors should trigger retries
  • Context Support: Full support for Go's context package for cancellation and timeouts
  • Multiple Retry Strategies: Convenient functions similar to the original C# library
  • Comprehensive Testing: Extensive test coverage for all functionality
  • Idempotent Operations: Designed for operations that can be safely retried

Installation

go get github.com/kriscoleman/GoRetry

Quick Start

Basic Usage
import "github.com/kriscoleman/GoRetry"

// Simple retry with default exponential backoff
err := goretry.IfNeeded(func() error {
    return someOperationThatMightFail()
})
Custom Transient Error Detection
err := goretry.IfNeeded(func() error {
    return cloudService.Post(credentials)
}, goretry.WithTransientErrorFunc(func(err error) bool {
    // Define your own logic for transient errors
    return strings.Contains(err.Error(), "timeout") || 
           strings.Contains(err.Error(), "connection refused")
}))
With Context
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()

err := goretry.IfNeededWithContext(ctx, func(ctx context.Context) error {
    return cloudService.GetMessages(ctx, credentials)
})

Retry Policies

Exponential Backoff (Default)
policy := goretry.NewExponentialBackoffPolicy(100*time.Millisecond, 5*time.Second)
retrier := goretry.NewRetrier(policy)
Fixed Delay
policy := goretry.NewFixedDelayPolicy(500 * time.Millisecond)
retrier := goretry.NewRetrier(policy)
Linear Backoff
policy := goretry.NewLinearBackoffPolicy(
    100*time.Millisecond, // base delay
    50*time.Millisecond,  // increment
    1*time.Second,        // max delay
)
retrier := goretry.NewRetrier(policy)
No Delay
policy := goretry.NewNoDelayPolicy()
retrier := goretry.NewRetrier(policy)
Stop Policy (Wrapper)
basePolicy := goretry.NewExponentialBackoffPolicy(100*time.Millisecond, 5*time.Second)
policy := goretry.NewStopPolicy(basePolicy).
    WithMaxAttempts(5).
    WithMaxDuration(30 * time.Second)

Configuration Options

Maximum Attempts
retrier := goretry.NewRetrier(policy, goretry.WithMaxAttempts(5))
Custom Transient Error Function
retrier := goretry.NewRetrier(policy, 
    goretry.WithTransientErrorFunc(func(err error) bool {
        // Your custom logic here
        return err.Error() == "temporary failure"
    }),
)
Retry Callback
retrier := goretry.NewRetrier(policy,
    goretry.WithOnRetry(func(attempt int, err error) {
        log.Printf("Retry attempt %d due to: %v", attempt, err)
    }),
)

Error Handling

When all retries are exhausted, GoRetry returns an OutOfRetriesError:

err := goretry.IfNeeded(func() error {
    return errors.New("persistent error")
})

var outOfRetriesErr *goretry.OutOfRetriesError
if errors.As(err, &outOfRetriesErr) {
    fmt.Printf("Failed after %d attempts\n", outOfRetriesErr.Attempts)
    fmt.Printf("Last error: %v\n", outOfRetriesErr.LastErr)
    
    // Access all errors that occurred
    for i, e := range outOfRetriesErr.AllErrs {
        fmt.Printf("Attempt %d error: %v\n", i+1, e)
    }
}

Default Transient Error Detection

The package includes a reasonable default for detecting transient errors:

  • Network timeout errors (Timeout() bool interface)
  • Temporary errors (Temporary() bool interface)
  • Context cancellation and deadline exceeded are not considered transient

Convenience Functions

IfNeeded - Basic retry with default settings
err := goretry.IfNeeded(func() error {
    return operation()
})
IfNeededWithContext - With context support
err := goretry.IfNeededWithContext(ctx, func(ctx context.Context) error {
    return operation(ctx)
})
IfNeededWithPolicy - With custom policy
policy := goretry.NewFixedDelayPolicy(200 * time.Millisecond)
err := goretry.IfNeededWithPolicy(policy, func() error {
    return operation()
})
IfNeededWithPolicyAndContext - Full control
err := goretry.IfNeededWithPolicyAndContext(ctx, policy, func(ctx context.Context) error {
    return operation(ctx)
})

Important Notes

⚠️ Idempotency Required: Like the original C# library, all retry operations must be idempotent to prevent unintended side effects during multiple retry attempts.

⚠️ Non-Transient Errors: When a non-transient error occurs, the retry mechanism immediately returns that error without further attempts.

Examples

See examples_test.go for more comprehensive examples of usage patterns.

License

This project is licensed under the MIT License - see the LICENSE file for details.

Inspiration

This library is inspired by the System.Retry C# library and follows MSDN's Retry Pattern guidelines.

Documentation

Overview

Package goretry provides convenient functions for common retry scenarios. These functions offer a simple API similar to the original C# System.Retry library.

Package goretry provides retry mechanisms with various backoff policies and transient error strategies. This file contains implementations of different retry policies that determine the delay between retry attempts.

Example (Policies)

Example showing different retry policies

package main

import (
	"fmt"
	"time"

	goretry "github.com/kriscoleman/GoRetry"
)

func main() {
	// Fixed delay - same delay between all retries
	fixedPolicy := goretry.NewFixedDelayPolicy(500 * time.Millisecond)

	// Exponential backoff with jitter
	expPolicy := goretry.NewExponentialBackoffPolicy(100*time.Millisecond, 10*time.Second).
		WithMultiplier(2.0).
		WithJitter(true)

	// Linear backoff
	linearPolicy := goretry.NewLinearBackoffPolicy(100*time.Millisecond, 100*time.Millisecond, 1*time.Second)

	// No delay - immediate retries
	noDelayPolicy := goretry.NewNoDelayPolicy()

	// Stop policy - wraps another policy with limits
	stopPolicy := goretry.NewStopPolicy(expPolicy).
		WithMaxAttempts(5).
		WithMaxDuration(30 * time.Second)

	policies := []goretry.RetryPolicy{
		fixedPolicy,
		expPolicy,
		linearPolicy,
		noDelayPolicy,
		stopPolicy,
	}

	for i, policy := range policies {
		retrier := goretry.NewRetrier(policy)
		_ = retrier // Use the retrier for actual retry logic
		fmt.Printf("Policy %d configured\n", i+1)
	}
}
Output:

Policy 1 configured
Policy 2 configured
Policy 3 configured
Policy 4 configured
Policy 5 configured

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

func DefaultTransientErrorFunc

func DefaultTransientErrorFunc(err error) bool

DefaultTransientErrorFunc provides a reasonable default implementation for transient error detection. It considers an error transient if it implements common Go network error interfaces:

  • Timeout() bool - for timeout errors
  • Temporary() bool - for temporary errors

Context cancellation and deadline exceeded errors are explicitly NOT considered transient, as they indicate intentional cancellation rather than temporary failures.

This function can be used as a starting point, but applications should consider implementing custom transient error logic based on their specific error types and requirements.

Example usage:

retrier := NewRetrier(policy, WithTransientErrorFunc(DefaultTransientErrorFunc))

func IfNeeded

func IfNeeded(fn func() error, options ...Option) error

IfNeeded provides a convenient way to retry a function with sensible default settings. This function is designed to mirror the C# library's Retry.IfNeeded method, providing a simple API for the most common retry scenarios.

Default configuration:

  • Exponential backoff starting at 100ms, max 5 seconds
  • Maximum 3 attempts
  • Uses DefaultTransientErrorFunc for error classification

The function will retry if the provided TransientErrorFunc (or default) returns true for the encountered error, up to the maximum number of attempts.

Example:

err := goretry.IfNeeded(func() error {
	return apiCall()
})

// With custom options
err := goretry.IfNeeded(func() error {
	return apiCall()
}, goretry.WithMaxAttempts(5), goretry.WithTransientErrorFunc(customFunc))
Example

Example demonstrating basic retry functionality

package main

import (
	"errors"
	"fmt"
	"log"

	goretry "github.com/kriscoleman/GoRetry"
)

func main() {
	callCount := 0

	err := goretry.IfNeeded(func() error {
		callCount++
		if callCount < 3 {
			return errors.New("transient network error")
		}
		return nil
	}, goretry.WithTransientErrorFunc(func(err error) bool {
		// Custom logic to determine if error is transient
		return err.Error() == "transient network error"
	}))

	if err != nil {
		log.Printf("Failed after retries: %v", err)
	} else {
		fmt.Printf("Success after %d attempts", callCount)
	}
}
Output:

Success after 3 attempts

func IfNeededWithContext

func IfNeededWithContext(ctx context.Context, fn func(context.Context) error, options ...Option) error

IfNeededWithContext provides a convenient way to retry a function with context support and default settings. This function combines the simplicity of IfNeeded with Go's context package for cancellation and timeout handling. The context is checked before each retry attempt.

Example:

ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()

err := goretry.IfNeededWithContext(ctx, func(ctx context.Context) error {
	return apiCallWithContext(ctx)
})

func IfNeededWithPolicy

func IfNeededWithPolicy(policy RetryPolicy, fn func() error, options ...Option) error

IfNeededWithPolicy provides a convenient way to retry with a specific retry policy. This function allows you to specify a custom retry policy while still using the convenient IfNeeded API.

Example:

policy := goretry.NewFixedDelayPolicy(500 * time.Millisecond)
err := goretry.IfNeededWithPolicy(policy, func() error {
	return apiCall()
}, goretry.WithMaxAttempts(5))

func IfNeededWithPolicyAndContext

func IfNeededWithPolicyAndContext(ctx context.Context, policy RetryPolicy, fn func(context.Context) error, options ...Option) error

IfNeededWithPolicyAndContext provides the most flexible convenient retry function, combining custom retry policy with context support. This gives you full control over retry behavior while maintaining the simple IfNeeded API.

Example:

ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
defer cancel()

policy := goretry.NewLinearBackoffPolicy(100*time.Millisecond, 100*time.Millisecond, 2*time.Second)
err := goretry.IfNeededWithPolicyAndContext(ctx, policy, func(ctx context.Context) error {
	return apiCallWithContext(ctx)
}, goretry.WithMaxAttempts(10))

Types

type ExponentialBackoffPolicy

type ExponentialBackoffPolicy struct {
	// BaseDelay is the initial delay for the first retry
	BaseDelay time.Duration
	// MaxDelay is the maximum delay that will be applied, providing an upper bound
	MaxDelay time.Duration
	// Multiplier is the factor by which the delay increases each attempt (default: 2.0)
	Multiplier float64
	// Jitter adds randomness to delays to prevent thundering herd (default: true)
	Jitter bool
}

ExponentialBackoffPolicy implements exponential backoff with optional jitter. This policy increases the delay exponentially with each retry attempt, which helps reduce load on failing services and increases the chance of recovery. Jitter can be enabled to add randomness and prevent thundering herd problems.

The delay is calculated as: BaseDelay * (Multiplier ^ (attempt-1)) If jitter is enabled, the actual delay will be randomized between delay/2 and delay.

Example:

policy := NewExponentialBackoffPolicy(100*time.Millisecond, 30*time.Second).
	WithMultiplier(2.0).
	WithJitter(true)

func NewExponentialBackoffPolicy

func NewExponentialBackoffPolicy(baseDelay, maxDelay time.Duration) *ExponentialBackoffPolicy

NewExponentialBackoffPolicy creates a new ExponentialBackoffPolicy with sensible defaults. The default multiplier is 2.0 and jitter is enabled by default.

Parameters:

  • baseDelay: the initial delay for the first retry attempt
  • maxDelay: the maximum delay that will be applied (prevents infinite growth)

Example:

policy := NewExponentialBackoffPolicy(100*time.Millisecond, 10*time.Second)

func (*ExponentialBackoffPolicy) NextDelay

func (p *ExponentialBackoffPolicy) NextDelay(attempt int) (time.Duration, bool)

NextDelay calculates the delay for the next retry attempt using exponential backoff. The delay grows exponentially with each attempt, capped at MaxDelay. If jitter is enabled, the delay is randomized between delay/2 and delay using cryptographically secure randomness.

func (*ExponentialBackoffPolicy) WithJitter

WithJitter controls whether jitter (randomness) is applied to delays. Jitter is enabled by default and helps prevent thundering herd problems when multiple clients retry simultaneously. Returns the policy instance for method chaining.

Example:

policy := NewExponentialBackoffPolicy(100*time.Millisecond, 10*time.Second).
	WithJitter(false) // Disable jitter for predictable delays

func (*ExponentialBackoffPolicy) WithMultiplier

func (p *ExponentialBackoffPolicy) WithMultiplier(multiplier float64) *ExponentialBackoffPolicy

WithMultiplier sets the multiplier used for exponential growth. The default multiplier is 2.0. Common values are between 1.5 and 3.0. Returns the policy instance for method chaining.

Example:

policy := NewExponentialBackoffPolicy(100*time.Millisecond, 10*time.Second).
	WithMultiplier(1.5) // Slower growth than default

type FixedDelayPolicy

type FixedDelayPolicy struct {
	// Delay is the fixed duration to wait between retry attempts
	Delay time.Duration
}

FixedDelayPolicy implements a retry policy with a constant delay between attempts. This policy provides the simplest retry strategy where each retry waits the same amount of time. It's useful when you want predictable, consistent timing between retries.

Example:

policy := NewFixedDelayPolicy(500 * time.Millisecond)
retrier := NewRetrier(policy)

func NewFixedDelayPolicy

func NewFixedDelayPolicy(delay time.Duration) *FixedDelayPolicy

NewFixedDelayPolicy creates a new FixedDelayPolicy with the specified delay duration. The delay will be applied between each retry attempt.

Example:

policy := NewFixedDelayPolicy(1 * time.Second) // 1 second between each retry

func (*FixedDelayPolicy) NextDelay

func (p *FixedDelayPolicy) NextDelay(attempt int) (time.Duration, bool)

NextDelay returns the fixed delay for any attempt number. It always returns the configured delay and true (indicating retries should continue).

type LinearBackoffPolicy

type LinearBackoffPolicy struct {
	BaseDelay time.Duration
	MaxDelay  time.Duration
	Increment time.Duration
}

LinearBackoffPolicy implements linear backoff

func NewLinearBackoffPolicy

func NewLinearBackoffPolicy(baseDelay, increment, maxDelay time.Duration) *LinearBackoffPolicy

func (*LinearBackoffPolicy) NextDelay

func (p *LinearBackoffPolicy) NextDelay(attempt int) (time.Duration, bool)

type NoDelayPolicy

type NoDelayPolicy struct{}

NoDelayPolicy implements immediate retry with no delay

func NewNoDelayPolicy

func NewNoDelayPolicy() *NoDelayPolicy

func (*NoDelayPolicy) NextDelay

func (p *NoDelayPolicy) NextDelay(attempt int) (time.Duration, bool)

type Option

type Option func(*Retrier)

Option is a function that configures a Retrier instance. Options are applied during Retrier creation to customize its behavior.

func WithMaxAttempts

func WithMaxAttempts(attempts int) Option

WithMaxAttempts sets the maximum number of retry attempts. The total number of function calls will be maxAttempts (including the initial attempt).

Example:

retrier := NewRetrier(policy, WithMaxAttempts(5)) // Will try up to 5 times total

func WithOnRetry

func WithOnRetry(fn func(attempt int, err error)) Option

WithOnRetry sets a callback function that is called before each retry attempt. This can be useful for logging, metrics collection, or other side effects. The callback receives the attempt number (1-based) and the error that triggered the retry.

Note: The callback is not called before the initial attempt, only before retries.

Example:

retrier := NewRetrier(policy, WithOnRetry(func(attempt int, err error) {
	log.Printf("Retry attempt %d due to error: %v", attempt, err)
}))

func WithTransientErrorFunc

func WithTransientErrorFunc(fn TransientErrorFunc) Option

WithTransientErrorFunc sets a custom function to determine if an error is transient. This function is called for each error encountered to decide whether a retry should be attempted. If the function returns true, the error is considered transient and a retry will be attempted (subject to other constraints like maximum attempts).

Example:

retrier := NewRetrier(policy, WithTransientErrorFunc(func(err error) bool {
	return strings.Contains(err.Error(), "temporary") ||
	       strings.Contains(err.Error(), "timeout")
}))

type OutOfRetriesError

type OutOfRetriesError struct {
	// Attempts is the total number of attempts made
	Attempts int
	// LastErr is the error from the final attempt
	LastErr error
	// AllErrs contains all errors encountered during retry attempts
	AllErrs []error
}

OutOfRetriesError is returned when all retry attempts are exhausted without success. It contains information about all attempts made and provides access to both the last error encountered and all errors that occurred during the retry process.

This error type implements the error interface and can be unwrapped to access the last error.

Example:

var outOfRetriesErr *OutOfRetriesError
if errors.As(err, &outOfRetriesErr) {
	fmt.Printf("Failed after %d attempts\n", outOfRetriesErr.Attempts)
	fmt.Printf("Last error: %v\n", outOfRetriesErr.LastErr)
}

func (*OutOfRetriesError) Error

func (e *OutOfRetriesError) Error() string

Error returns a string representation of the OutOfRetriesError.

func (*OutOfRetriesError) Unwrap

func (e *OutOfRetriesError) Unwrap() error

Unwrap returns the last error, allowing for error unwrapping with errors.Is and errors.As.

type Retrier

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

Retrier provides retry functionality with configurable policies and transient error strategies. It encapsulates the retry logic and can be configured with various options such as maximum attempts, custom transient error detection, and retry callbacks.

Retrier is safe for concurrent use by multiple goroutines.

Example:

retrier := NewRetrier(
	NewExponentialBackoffPolicy(100*time.Millisecond, 5*time.Second),
	WithMaxAttempts(5),
	WithTransientErrorFunc(func(err error) bool {
		return strings.Contains(err.Error(), "temporary")
	}),
)

err := retrier.Do(func() error {
	return riskyOperation()
})
Example (CustomTransientErrors)

Example with custom transient error detection

package main

import (
	"errors"
	"fmt"
	"log"
	"time"

	goretry "github.com/kriscoleman/GoRetry"
)

func main() {
	// Define what errors should trigger a retry
	isTransient := func(err error) bool {
		if err == nil {
			return false
		}

		// Retry on specific error messages
		msg := err.Error()
		return msg == "connection refused" ||
			msg == "timeout" ||
			msg == "temporary failure"
	}

	retrier := goretry.NewRetrier(
		goretry.NewLinearBackoffPolicy(100*time.Millisecond, 50*time.Millisecond, 500*time.Millisecond),
		goretry.WithTransientErrorFunc(isTransient),
		goretry.WithMaxAttempts(3),
		goretry.WithOnRetry(func(attempt int, err error) {
			log.Printf("Retry attempt %d due to: %v", attempt, err)
		}),
	)

	err := retrier.Do(func() error {
		// Simulate different types of errors
		return errors.New("connection refused")
	})

	if err != nil {
		fmt.Printf("Final error: %v", err)
	}
}
Example (ExponentialBackoff)

Example with exponential backoff policy

package main

import (
	"fmt"
	"net/http"
	"time"

	goretry "github.com/kriscoleman/GoRetry"
)

func main() {
	policy := goretry.NewExponentialBackoffPolicy(100*time.Millisecond, 2*time.Second)
	retrier := goretry.NewRetrier(policy, goretry.WithMaxAttempts(5))

	err := retrier.Do(func() error {
		// Simulate a service call that might fail
		resp, err := http.Get("https://httpbin.org/status/500")
		if err != nil {
			return err
		}
		defer resp.Body.Close()

		if resp.StatusCode >= 500 {
			return fmt.Errorf("server error: %d", resp.StatusCode)
		}
		return nil
	})

	if err != nil {
		fmt.Printf("Request failed: %v", err)
	}
}
Example (WithContext)

Example with context cancellation

package main

import (
	"context"
	"errors"
	"fmt"
	"time"

	goretry "github.com/kriscoleman/GoRetry"
)

func main() {
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()

	retrier := goretry.NewRetrier(goretry.NewFixedDelayPolicy(200 * time.Millisecond))

	err := retrier.DoWithContext(ctx, func(ctx context.Context) error {
		select {
		case <-ctx.Done():
			return ctx.Err()
		default:
			// Simulate work that might fail
			return errors.New("simulated failure")
		}
	})

	if err != nil {
		fmt.Printf("Operation failed or timed out: %v", err)
	}
}

func NewRetrier

func NewRetrier(policy RetryPolicy, options ...Option) *Retrier

NewRetrier creates a new Retrier with the specified retry policy and optional configuration. The retry policy determines the delay strategy between attempts. Additional options can be provided to customize behavior such as maximum attempts, transient error detection, and retry callbacks.

Default configuration:

  • Maximum attempts: 3
  • Transient error function: DefaultTransientErrorFunc
  • No retry callback

Example:

retrier := NewRetrier(
	NewExponentialBackoffPolicy(100*time.Millisecond, 5*time.Second),
	WithMaxAttempts(5),
	WithTransientErrorFunc(customTransientFunc),
)

func (*Retrier) Do

func (r *Retrier) Do(fn func() error) error

Do executes the given function with retry logic. The function will be retried according to the configured policy and options until it succeeds, a non-transient error occurs, or the maximum attempts are reached.

The function should be idempotent as it may be called multiple times. If all retry attempts are exhausted, an OutOfRetriesError is returned.

Example:

err := retrier.Do(func() error {
	resp, err := http.Get("https://api.example.com/data")
	if err != nil {
		return err
	}
	defer resp.Body.Close()
	return processResponse(resp)
})

func (*Retrier) DoWithContext

func (r *Retrier) DoWithContext(ctx context.Context, fn func(context.Context) error) error

DoWithContext executes the given function with retry logic and context support. This method provides the same retry functionality as Do, but with context support for cancellation and timeout handling.

The context is checked before each retry attempt, and if cancelled or timed out, the context error is returned immediately without further retries.

Example:

ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()

err := retrier.DoWithContext(ctx, func(ctx context.Context) error {
	req, _ := http.NewRequestWithContext(ctx, "GET", "https://api.example.com/data", nil)
	resp, err := client.Do(req)
	if err != nil {
		return err
	}
	defer resp.Body.Close()
	return processResponse(resp)
})

type RetryPolicy

type RetryPolicy interface {
	// NextDelay calculates the delay before the next retry attempt.
	// attempt is 1-based (first retry is attempt 1).
	// Returns the delay duration and whether retrying should continue.
	NextDelay(attempt int) (time.Duration, bool)
}

RetryPolicy defines the retry behavior and delay calculation strategy. Implementations determine how long to wait between retry attempts and whether to continue retrying.

The NextDelay method receives the current attempt number (1-based) and returns:

  • delay: duration to wait before the next attempt
  • shouldContinue: whether to proceed with another attempt

Common implementations include exponential backoff, fixed delays, and linear backoff.

type StopPolicy

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

StopPolicy wraps another policy and stops retrying after a specified condition. Note: For duration-based stopping, timing is managed by the retrier, not the policy, to ensure thread safety and correct timing behavior.

func NewStopPolicy

func NewStopPolicy(policy RetryPolicy) *StopPolicy

func (*StopPolicy) GetMaxDuration

func (p *StopPolicy) GetMaxDuration() time.Duration

GetMaxDuration returns the maximum duration setting for use by the retrier

func (*StopPolicy) NextDelay

func (p *StopPolicy) NextDelay(attempt int) (time.Duration, bool)

func (*StopPolicy) WithMaxAttempts

func (p *StopPolicy) WithMaxAttempts(attempts int) *StopPolicy

func (*StopPolicy) WithMaxDuration

func (p *StopPolicy) WithMaxDuration(duration time.Duration) *StopPolicy

type TransientErrorFunc

type TransientErrorFunc func(error) bool

TransientErrorFunc is a function that determines if an error is transient and should trigger a retry. It receives an error and returns true if the error is considered transient (temporary/recoverable), false if the error is persistent and retries should not be attempted.

Example:

func isNetworkError(err error) bool {
	return strings.Contains(err.Error(), "connection refused") ||
	       strings.Contains(err.Error(), "timeout")
}

Jump to

Keyboard shortcuts

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