Skip to content

Commit

Permalink
fix(fmc): adding retry mechanism with exponential backoff
Browse files Browse the repository at this point in the history
  • Loading branch information
bl4ko committed Aug 22, 2024
1 parent abf2dcd commit 018e496
Showing 1 changed file with 75 additions and 0 deletions.
75 changes: 75 additions & 0 deletions internal/source/fmc/fmc_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"encoding/json"
"fmt"
"io"
"math"
"net/http"
"time"

Expand All @@ -22,6 +23,13 @@ type fmcClient struct {
DefaultTimeout time.Duration
}

const (
maxRetries = 5
initialBackoff = 500 * time.Millisecond
backoffFactor = 2.0
maxBackoff = 16 * time.Second
)

func newFMCClient(username string, password string, httpScheme string, hostname string, port int, httpClient *http.Client) (*fmcClient, error) {
// First we obtain access and refresh token
c := &fmcClient{
Expand All @@ -43,8 +51,36 @@ func newFMCClient(username string, password string, httpScheme string, hostname
return c, nil
}

// exponentialBackoff calculates the backoff duration based on the number of attempts.
func exponentialBackoff(attempt int) time.Duration {
backoff := time.Duration(float64(initialBackoff) * math.Pow(backoffFactor, float64(attempt)))
if backoff > maxBackoff {
backoff = maxBackoff

Check warning on line 58 in internal/source/fmc/fmc_client.go

View check run for this annotation

Codecov / codecov/patch

internal/source/fmc/fmc_client.go#L55-L58

Added lines #L55 - L58 were not covered by tests
}
return backoff

Check warning on line 60 in internal/source/fmc/fmc_client.go

View check run for this annotation

Codecov / codecov/patch

internal/source/fmc/fmc_client.go#L60

Added line #L60 was not covered by tests
}

// Authenticate performs authentication on FMC API. If successful it returns access and refresh tokens.
func (fmcc fmcClient) Authenticate() (string, string, error) {
var (
accessToken string
refreshToken string
err error
)

Check warning on line 69 in internal/source/fmc/fmc_client.go

View check run for this annotation

Codecov / codecov/patch

internal/source/fmc/fmc_client.go#L65-L69

Added lines #L65 - L69 were not covered by tests

for attempt := 0; attempt < maxRetries; attempt++ {
accessToken, refreshToken, err = fmcc.authenticateOnce()
if err == nil {
return accessToken, refreshToken, nil

Check warning on line 74 in internal/source/fmc/fmc_client.go

View check run for this annotation

Codecov / codecov/patch

internal/source/fmc/fmc_client.go#L71-L74

Added lines #L71 - L74 were not covered by tests
}

time.Sleep(exponentialBackoff(attempt))

Check warning on line 77 in internal/source/fmc/fmc_client.go

View check run for this annotation

Codecov / codecov/patch

internal/source/fmc/fmc_client.go#L77

Added line #L77 was not covered by tests
}

return "", "", fmt.Errorf("authentication failed after %d attempts: %w", maxRetries, err)

Check warning on line 80 in internal/source/fmc/fmc_client.go

View check run for this annotation

Codecov / codecov/patch

internal/source/fmc/fmc_client.go#L80

Added line #L80 was not covered by tests
}

func (fmcc fmcClient) authenticateOnce() (string, string, error) {

Check warning on line 83 in internal/source/fmc/fmc_client.go

View check run for this annotation

Codecov / codecov/patch

internal/source/fmc/fmc_client.go#L83

Added line #L83 was not covered by tests
ctx, cancel := context.WithTimeout(context.Background(), fmcc.DefaultTimeout)
defer cancel()
req, err := http.NewRequestWithContext(ctx, http.MethodPost, fmt.Sprintf("%s/fmc_platform/v1/auth/generatetoken", fmcc.BaseURL), nil)
Expand Down Expand Up @@ -101,7 +137,39 @@ type Device struct {
Name string `json:"name"`
}

// MakeRequest sends an HTTP request to the specified path using the given method and body.
// It retries the request with exponential backoff up to a maximum number of attempts.
// If the request fails after the maximum number of attempts, it returns an error.
//
// Parameters:
// - ctx: The context.Context for the request.
// - method: The HTTP method to use for the request (e.g., GET, POST, PUT, DELETE).
// - path: The path of the resource to request.
// - body: The request body as an io.Reader.
//
// Returns:
// - *http.Response: The HTTP response if the request is successful.
// - error: An error if the request fails after the maximum number of attempts.
func (fmcc *fmcClient) MakeRequest(ctx context.Context, method, path string, body io.Reader) (*http.Response, error) {
var (
resp *http.Response
err error
)

Check warning on line 157 in internal/source/fmc/fmc_client.go

View check run for this annotation

Codecov / codecov/patch

internal/source/fmc/fmc_client.go#L154-L157

Added lines #L154 - L157 were not covered by tests

for attempt := 0; attempt < maxRetries; attempt++ {
resp, err = fmcc.makeRequestOnce(ctx, method, path, body)
if err == nil {
return resp, nil

Check warning on line 162 in internal/source/fmc/fmc_client.go

View check run for this annotation

Codecov / codecov/patch

internal/source/fmc/fmc_client.go#L159-L162

Added lines #L159 - L162 were not covered by tests
}
time.Sleep(exponentialBackoff(attempt))

Check warning on line 164 in internal/source/fmc/fmc_client.go

View check run for this annotation

Codecov / codecov/patch

internal/source/fmc/fmc_client.go#L164

Added line #L164 was not covered by tests
}

return nil, fmt.Errorf("request failed after %d attempts: %w", maxRetries, err)

Check warning on line 167 in internal/source/fmc/fmc_client.go

View check run for this annotation

Codecov / codecov/patch

internal/source/fmc/fmc_client.go#L167

Added line #L167 was not covered by tests
}

// makeRequestOnce sends an HTTP request to the specified path using the given method and body.
// It is a helper function for MakeRequest that sends the request only once.
func (fmcc *fmcClient) makeRequestOnce(ctx context.Context, method, path string, body io.Reader) (*http.Response, error) {

Check warning on line 172 in internal/source/fmc/fmc_client.go

View check run for this annotation

Codecov / codecov/patch

internal/source/fmc/fmc_client.go#L172

Added line #L172 was not covered by tests
ctxWithTimeout, cancel := context.WithTimeout(ctx, fmcc.DefaultTimeout)
defer cancel()
req, err := http.NewRequestWithContext(ctxWithTimeout, method, fmt.Sprintf("%s/%s", fmcc.BaseURL, path), body)
Expand All @@ -113,6 +181,8 @@ func (fmcc *fmcClient) MakeRequest(ctx context.Context, method, path string, bod
return fmcc.HTTPClient.Do(req)
}

// GetDomains returns a list of domains from the FMC API.
// It sends a GET request to the /fmc_platform/v1/info/domain endpoint.
func (fmcc *fmcClient) GetDomains() ([]Domain, error) {
offset := 0
limit := 25
Expand Down Expand Up @@ -149,6 +219,7 @@ func (fmcc *fmcClient) GetDomains() ([]Domain, error) {
return domains, nil
}

// GetDevices returns a list of devices from the FMC API for the specified domain.
func (fmcc *fmcClient) GetDevices(domainUUID string) ([]Device, error) {
offset := 0
limit := 25
Expand Down Expand Up @@ -192,6 +263,7 @@ type PhysicalInterface struct {
Name string `json:"name"`
}

// GetDevicePhysicalInterfaces returns a list of physical interfaces for the specified device in the specified domain.
func (fmcc *fmcClient) GetDevicePhysicalInterfaces(domainUUID string, deviceID string) ([]PhysicalInterface, error) {
offset := 0
limit := 25
Expand Down Expand Up @@ -347,6 +419,7 @@ func (fmcc *fmcClient) GetVLANInterfaceInfo(domainUUID string, deviceID string,
return &vlanInterfaceInfo, nil
}

// VLANInterfaceInfo represents information about a VLAN interface.
type VLANInterfaceInfo struct {
Type string `json:"type"`
Mode string `json:"mode"`
Expand Down Expand Up @@ -375,6 +448,7 @@ type VLANInterfaceInfo struct {
} `json:"ipv6"`
}

// DeviceInfo represents information about a FMC device.
type DeviceInfo struct {
Name string `json:"name"`
Description string `json:"description"`
Expand All @@ -393,6 +467,7 @@ type DeviceInfo struct {
} `json:"metadata"`
}

// GetDeviceInfo returns information about a device in the specified domain.
func (fmcc *fmcClient) GetDeviceInfo(domainUUID string, deviceID string) (*DeviceInfo, error) {
var deviceInfo DeviceInfo
ctx := context.Background()
Expand Down

0 comments on commit 018e496

Please sign in to comment.