This repository has been archived by the owner on Apr 5, 2023. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 6
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Signed-off-by: Firas Qutishat <firas.qutishat@securekey.com>
- Loading branch information
Showing
6 changed files
with
575 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,91 @@ | ||
/* | ||
Copyright SecureKey Technologies Inc. All Rights Reserved. | ||
SPDX-License-Identifier: Apache-2.0 | ||
*/ | ||
|
||
package retry | ||
|
||
import ( | ||
"errors" | ||
"time" | ||
) | ||
|
||
type retryOpts struct { | ||
MaxAttempts int | ||
InitialBackoff time.Duration | ||
BackoffFactor float32 | ||
MaxBackoff time.Duration | ||
BeforeRetry BeforeRetryHandler | ||
} | ||
|
||
// BeforeRetryHandler is a function that is invoked before a retry attemp. | ||
// Return true to perform the retry; false otherwise. | ||
type BeforeRetryHandler func(err error, attempt int, backoff time.Duration) bool | ||
|
||
// Opt is a retry option | ||
type Opt func(opts *retryOpts) | ||
|
||
// WithMaxAttempts sets the maximum number of retry attempts | ||
func WithMaxAttempts(value int) Opt { | ||
return func(opts *retryOpts) { | ||
opts.MaxAttempts = value | ||
} | ||
} | ||
|
||
// WithBeforeRetry sets the handler to be invoked before a retry | ||
func WithBeforeRetry(value BeforeRetryHandler) Opt { | ||
return func(opts *retryOpts) { | ||
opts.BeforeRetry = value | ||
} | ||
} | ||
|
||
// Invocation is the function to invoke on each attempt | ||
type Invocation func() (interface{}, error) | ||
|
||
// Invoke invokes the given invocation with the given retry options | ||
func Invoke(invoke Invocation, opts ...Opt) (interface{}, error) { | ||
retryOpts := &retryOpts{ | ||
MaxAttempts: 5, | ||
BackoffFactor: 1.5, | ||
InitialBackoff: 250 * time.Millisecond, | ||
MaxBackoff: 5 * time.Second, | ||
} | ||
|
||
// Apply the options | ||
for _, opt := range opts { | ||
opt(retryOpts) | ||
} | ||
|
||
if retryOpts.MaxAttempts == 0 { | ||
return nil, errors.New("MaxAttempts must be greater than 0") | ||
} | ||
|
||
backoff := retryOpts.InitialBackoff | ||
var lastErr error | ||
var retVal interface{} | ||
for i := 1; i <= retryOpts.MaxAttempts; i++ { | ||
retVal, lastErr = invoke() | ||
if lastErr == nil { | ||
return retVal, nil | ||
} | ||
|
||
if i+1 < retryOpts.MaxAttempts { | ||
backoff = time.Duration(float32(backoff) * retryOpts.BackoffFactor) | ||
if backoff > retryOpts.MaxBackoff { | ||
backoff = retryOpts.MaxBackoff | ||
} | ||
|
||
if retryOpts.BeforeRetry != nil { | ||
if !retryOpts.BeforeRetry(lastErr, i, backoff) { | ||
// No retry for this error | ||
return nil, lastErr | ||
} | ||
} | ||
|
||
time.Sleep(backoff) | ||
} | ||
} | ||
|
||
return nil, lastErr | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,74 @@ | ||
/* | ||
Copyright SecureKey Technologies Inc. All Rights Reserved. | ||
SPDX-License-Identifier: Apache-2.0 | ||
*/ | ||
|
||
package retry | ||
|
||
import ( | ||
"testing" | ||
"time" | ||
|
||
"github.com/pkg/errors" | ||
"github.com/stretchr/testify/require" | ||
) | ||
|
||
func TestWithMaxAttempt(t *testing.T) { | ||
count := 0 | ||
_, err := Invoke( | ||
func() (interface{}, error) { | ||
count++ | ||
return nil, errors.New("error") | ||
|
||
}, | ||
WithMaxAttempts(4), | ||
) | ||
|
||
require.Error(t, err) | ||
require.Equal(t, 4, count) | ||
|
||
count = 0 | ||
v, err := Invoke( | ||
func() (interface{}, error) { | ||
count++ | ||
if count == 4 { | ||
return "success", nil | ||
} | ||
return nil, errors.New("error") | ||
|
||
}, | ||
WithMaxAttempts(4), | ||
) | ||
|
||
require.NoError(t, err) | ||
require.Equal(t, 4, count) | ||
require.Equal(t, "success", v) | ||
} | ||
|
||
func TestWithBeforeRetry(t *testing.T) { | ||
count := 0 | ||
_, err := Invoke( | ||
func() (interface{}, error) { | ||
count++ | ||
if count == 2 { | ||
return nil, errors.New("noretry") | ||
} | ||
return nil, errors.New("retry") | ||
|
||
}, | ||
WithBeforeRetry(func(err error, attempt int, backoff time.Duration) bool { | ||
require.Error(t, err) | ||
if err.Error() == "retry" { | ||
return true | ||
} | ||
require.Equal(t, "noretry", err.Error()) | ||
require.Equal(t, 2, attempt) | ||
return false | ||
}), | ||
WithMaxAttempts(4), | ||
) | ||
|
||
require.Error(t, err) | ||
require.Equal(t, 2, count) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,153 @@ | ||
/* | ||
Copyright SecureKey Technologies Inc. All Rights Reserved. | ||
SPDX-License-Identifier: Apache-2.0 | ||
*/ | ||
|
||
package couchdb | ||
|
||
import ( | ||
"net/http" | ||
"strings" | ||
"time" | ||
|
||
"github.com/hyperledger/fabric/common/util/retry" | ||
"github.com/pkg/errors" | ||
) | ||
|
||
// CreateNewIndexWithRetry method provides a function creating an index but retries on failure | ||
func (dbclient *CouchDatabase) CreateNewIndexWithRetry(indexdefinition string, designDoc string) error { | ||
//get the number of retries | ||
maxRetries := dbclient.CouchInstance.conf.MaxRetries | ||
|
||
_, err := retry.Invoke( | ||
func() (interface{}, error) { | ||
exists, err := dbclient.IndexDesignDocExists(designDoc) | ||
if err != nil { | ||
return nil, err | ||
} | ||
if exists { | ||
return nil, nil | ||
} | ||
|
||
return dbclient.CreateIndex(indexdefinition) | ||
}, | ||
retry.WithMaxAttempts(maxRetries), | ||
) | ||
return err | ||
} | ||
|
||
// Exists determines if the database exists | ||
func (dbclient *CouchDatabase) Exists() (bool, error) { | ||
_, dbReturn, err := dbclient.GetDatabaseInfo() | ||
if dbReturn != nil && dbReturn.StatusCode == http.StatusNotFound { | ||
return false, nil | ||
} | ||
if err != nil { | ||
return false, err | ||
} | ||
return true, nil | ||
} | ||
|
||
var errDBNotFound = errors.Errorf("DB not found") | ||
|
||
func isPvtDataDB(dbName string) bool { | ||
return strings.Contains(dbName, "$$h") || strings.Contains(dbName, "$$p") | ||
} | ||
|
||
func (dbclient *CouchDatabase) shouldRetry(err error) bool { | ||
return err == errDBNotFound && !isPvtDataDB(dbclient.DBName) | ||
} | ||
|
||
// ExistsWithRetry determines if the database exists, but retries until it does | ||
func (dbclient *CouchDatabase) ExistsWithRetry() (bool, error) { | ||
//get the number of retries | ||
maxRetries := dbclient.CouchInstance.conf.MaxRetries | ||
|
||
_, err := retry.Invoke( | ||
func() (interface{}, error) { | ||
dbExists, err := dbclient.Exists() | ||
if err != nil { | ||
return nil, err | ||
} | ||
if !dbExists { | ||
return nil, errDBNotFound | ||
} | ||
|
||
// DB exists | ||
return nil, nil | ||
}, | ||
retry.WithMaxAttempts(maxRetries), | ||
retry.WithBeforeRetry(func(err error, attempt int, backoff time.Duration) bool { | ||
if dbclient.shouldRetry(err) { | ||
logger.Debugf("Got error [%s] checking if DB [%s] exists on attempt #%d. Will retry in %s.", err, dbclient.DBName, attempt, backoff) | ||
return true | ||
} | ||
logger.Debugf("Got error [%s] checking if DB [%s] exists on attempt #%d. Will NOT retry", err, dbclient.DBName, attempt) | ||
return false | ||
}), | ||
) | ||
|
||
if err != nil { | ||
if err == errDBNotFound { | ||
return false, nil | ||
} | ||
return false, err | ||
} | ||
|
||
return true, nil | ||
} | ||
|
||
// IndexDesignDocExists determines if all the passed design documents exists in the database. | ||
func (dbclient *CouchDatabase) IndexDesignDocExists(designDocs ...string) (bool, error) { | ||
designDocExists := make([]bool, len(designDocs)) | ||
|
||
indices, err := dbclient.ListIndex() | ||
if err != nil { | ||
return false, errors.WithMessage(err, "retrieval of DB index list failed") | ||
} | ||
|
||
for _, dbIndexDef := range indices { | ||
for j, docName := range designDocs { | ||
if dbIndexDef.DesignDocument == docName { | ||
designDocExists[j] = true | ||
} | ||
} | ||
} | ||
|
||
for _, exists := range designDocExists { | ||
if !exists { | ||
return false, nil | ||
} | ||
} | ||
|
||
return true, nil | ||
} | ||
|
||
// IndexDesignDocExists determines if all the passed design documents exists in the database. | ||
func (dbclient *CouchDatabase) IndexDesignDocExistsWithRetry(designDocs ...string) (bool, error) { | ||
//get the number of retries | ||
maxRetries := dbclient.CouchInstance.conf.MaxRetries | ||
|
||
_, err := retry.Invoke( | ||
func() (interface{}, error) { | ||
indexExists, err := dbclient.IndexDesignDocExists(designDocs...) | ||
if err != nil { | ||
return nil, err | ||
} | ||
if !indexExists { | ||
return nil, errors.Errorf("DB index not found: [%s]", dbclient.DBName) | ||
} | ||
|
||
// DB index exists | ||
return nil, nil | ||
}, | ||
retry.WithMaxAttempts(maxRetries), | ||
) | ||
|
||
if err != nil { | ||
return false, err | ||
} | ||
|
||
return true, nil | ||
} |
Oops, something went wrong.