Skip to content
This repository has been archived by the owner on Apr 5, 2023. It is now read-only.

Commit

Permalink
[#22] Add couchdb helper methods
Browse files Browse the repository at this point in the history
Signed-off-by: Firas Qutishat <firas.qutishat@securekey.com>
  • Loading branch information
fqutishat committed May 5, 2019
1 parent 375e48a commit dbf261c
Show file tree
Hide file tree
Showing 6 changed files with 575 additions and 0 deletions.
91 changes: 91 additions & 0 deletions common/util/retry/retry.go
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
}
74 changes: 74 additions & 0 deletions common/util/retry/retry_test.go
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)
}
153 changes: 153 additions & 0 deletions core/ledger/util/couchdb/couchdb_ext.go
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
}
Loading

0 comments on commit dbf261c

Please sign in to comment.