Skip to content
This repository was archived by the owner on Apr 25, 2025. It is now read-only.

Commit 47e68c4

Browse files
committed
[FAB-7508] Add retries to channel client
Change-Id: I3bceb4913bfc4e2d539031ba46055a8d69edc304 Signed-off-by: Divyank Katira <Divyank.Katira@securekey.com>
1 parent e985ca4 commit 47e68c4

File tree

9 files changed

+322
-15
lines changed

9 files changed

+322
-15
lines changed

api/apitxn/opts.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ package apitxn
88

99
import (
1010
"time"
11+
12+
"github.com/hyperledger/fabric-sdk-go/pkg/retry"
1113
)
1214

1315
//WithTimeout encapsulates time.Duration to Option
@@ -25,3 +27,11 @@ func WithProposalProcessor(proposalProcessors ...ProposalProcessor) Option {
2527
return nil
2628
}
2729
}
30+
31+
// WithRetry option to configure retries
32+
func WithRetry(opt retry.Opts) Option {
33+
return func(opts *Opts) error {
34+
opts.Retry = opt
35+
return nil
36+
}
37+
}

api/apitxn/txn.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ package apitxn
99
import (
1010
"time"
1111

12+
"github.com/hyperledger/fabric-sdk-go/pkg/retry"
1213
pb "github.com/hyperledger/fabric-sdk-go/third_party/github.com/hyperledger/fabric/protos/peer"
1314
)
1415

@@ -33,6 +34,7 @@ type Response struct {
3334
type Opts struct {
3435
ProposalProcessors []ProposalProcessor // targets
3536
Timeout time.Duration
37+
Retry retry.Opts
3638
}
3739

3840
//Option func for each Opts argument

api/apitxn/txnhandler/txnhandler.go

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ package txnhandler
99
import (
1010
"github.com/hyperledger/fabric-sdk-go/api/apifabclient"
1111
"github.com/hyperledger/fabric-sdk-go/api/apitxn"
12+
"github.com/hyperledger/fabric-sdk-go/pkg/retry"
1213
)
1314

1415
//Handler for chaining transaction executions
@@ -26,7 +27,8 @@ type ClientContext struct {
2627

2728
//RequestContext contains request, opts, response parameters for handler execution
2829
type RequestContext struct {
29-
Request apitxn.Request
30-
Opts apitxn.Opts
31-
Response apitxn.Response
30+
Request apitxn.Request
31+
Opts apitxn.Opts
32+
Response apitxn.Response
33+
RetryHandler retry.Handler
3234
}

pkg/fabric-client/mocks/mockpeer.go

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,16 @@ import (
1616

1717
// MockPeer is a mock fabricsdk.Peer.
1818
type MockPeer struct {
19-
Error error
20-
MockName string
21-
MockURL string
22-
MockRoles []string
23-
MockCert *pem.Block
24-
Payload []byte
25-
ResponseMessage string
26-
MockMSP string
27-
Status int32
19+
Error error
20+
MockName string
21+
MockURL string
22+
MockRoles []string
23+
MockCert *pem.Block
24+
Payload []byte
25+
ResponseMessage string
26+
MockMSP string
27+
Status int32
28+
ProcessProposalCalls int
2829
}
2930

3031
// NewMockPeer creates basic mock peer
@@ -80,6 +81,7 @@ func (p *MockPeer) URL() string {
8081

8182
// ProcessTransactionProposal does not send anything anywhere but returns an empty mock ProposalResponse
8283
func (p *MockPeer) ProcessTransactionProposal(tp apitxn.TransactionProposal) (apitxn.TransactionProposalResult, error) {
84+
p.ProcessProposalCalls++
8385
return apitxn.TransactionProposalResult{
8486
Endorser: p.MockURL,
8587
Proposal: tp,

pkg/fabric-txn/chclient/chclient.go

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import (
1818
"github.com/hyperledger/fabric-sdk-go/api/apitxn/txnhandler"
1919
"github.com/hyperledger/fabric-sdk-go/pkg/errors"
2020
txnHandlerImpl "github.com/hyperledger/fabric-sdk-go/pkg/fabric-txn/txnhandler"
21+
"github.com/hyperledger/fabric-sdk-go/pkg/retry"
2122
"github.com/hyperledger/fabric-sdk-go/pkg/status"
2223
)
2324

@@ -85,8 +86,12 @@ func (cc *ChannelClient) InvokeHandler(handler txnhandler.Handler, request apitx
8586
complete := make(chan bool)
8687

8788
go func() {
89+
handleInvoke:
8890
//Perform action through handler
8991
handler.Handle(requestContext, clientContext)
92+
if requestContext.RetryHandler.Required(requestContext.Response.Error) {
93+
goto handleInvoke
94+
}
9095
complete <- true
9196
}()
9297
select {
@@ -113,9 +118,10 @@ func (cc *ChannelClient) prepareHandlerContexts(request apitxn.Request, options
113118
}
114119

115120
requestContext := &txnhandler.RequestContext{
116-
Request: request,
117-
Opts: options,
118-
Response: apitxn.Response{},
121+
Request: request,
122+
Opts: options,
123+
Response: apitxn.Response{},
124+
RetryHandler: retry.New(options.Retry),
119125
}
120126

121127
if requestContext.Opts.Timeout == 0 {

pkg/fabric-txn/chclient/chclient_test.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import (
1919
"github.com/hyperledger/fabric-sdk-go/pkg/fabric-client/channel"
2020
fcmocks "github.com/hyperledger/fabric-sdk-go/pkg/fabric-client/mocks"
2121
"github.com/hyperledger/fabric-sdk-go/pkg/fabric-client/peer"
22+
"github.com/hyperledger/fabric-sdk-go/pkg/retry"
2223

2324
txnmocks "github.com/hyperledger/fabric-sdk-go/pkg/fabric-txn/mocks"
2425
"github.com/hyperledger/fabric-sdk-go/pkg/status"
@@ -281,6 +282,23 @@ func TestTransactionValidationError(t *testing.T) {
281282
assert.EqualValues(t, validationCode, status.ToTransactionValidationCode(statusError.Code))
282283
}
283284

285+
func TestExecuteTxWithRetries(t *testing.T) {
286+
testStatus := status.New(status.EndorserClientStatus, status.ConnectionFailed.ToInt32(), "test", nil)
287+
288+
testPeer1 := fcmocks.NewMockPeer("Peer1", "http://peer1.com")
289+
testPeer1.Error = testStatus
290+
chClient := setupChannelClient([]apifabclient.Peer{testPeer1}, t)
291+
retryOpts := retry.DefaultOpts
292+
retryOpts.RetryableCodes = retry.ChannelClientRetryableCodes
293+
294+
response := chClient.Query(apitxn.Request{ChaincodeID: "testCC", Fcn: "invoke", Args: [][]byte{[]byte("query"), []byte("b")}},
295+
apitxn.WithRetry(retryOpts))
296+
if response.Error == nil {
297+
t.Fatalf("Should have failed for not success status")
298+
}
299+
assert.Equal(t, retry.DefaultOpts.Attempts, testPeer1.ProcessProposalCalls-1, "Expected peer to be called (retry attempts + 1) times")
300+
}
301+
284302
func setupTestChannel() (*channel.Channel, error) {
285303
ctx := setupTestContext()
286304
return channel.New(ctx, "testChannel")

pkg/retry/defaults.go

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
/*
2+
Copyright SecureKey Technologies Inc. All Rights Reserved.
3+
4+
SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
package retry
8+
9+
import (
10+
"time"
11+
12+
"github.com/hyperledger/fabric-sdk-go/pkg/status"
13+
"github.com/hyperledger/fabric-sdk-go/third_party/github.com/hyperledger/fabric/protos/common"
14+
pb "github.com/hyperledger/fabric-sdk-go/third_party/github.com/hyperledger/fabric/protos/peer"
15+
grpcCodes "google.golang.org/grpc/codes"
16+
)
17+
18+
const (
19+
// DefaultAttempts number of retry attempts made by default
20+
DefaultAttempts = 3
21+
// DefaultInitialBackoff default initial backoff
22+
DefaultInitialBackoff = 500 * time.Millisecond
23+
// DefaultMaxBackoff default maximum backoff
24+
DefaultMaxBackoff = 60 * time.Second
25+
// DefaultBackoffFactor default backoff factor
26+
DefaultBackoffFactor = 2.0
27+
)
28+
29+
// DefaultOpts default retry options
30+
var DefaultOpts = Opts{
31+
Attempts: DefaultAttempts,
32+
InitialBackoff: DefaultInitialBackoff,
33+
MaxBackoff: DefaultMaxBackoff,
34+
BackoffFactor: DefaultBackoffFactor,
35+
RetryableCodes: DefaultRetryableCodes,
36+
}
37+
38+
// DefaultRetryableCodes these are the error codes, grouped by source of error,
39+
// that are considered to be transient error conditions by default
40+
var DefaultRetryableCodes = map[status.Group][]status.Code{
41+
status.EndorserClientStatus: []status.Code{
42+
status.EndorsementMismatch,
43+
},
44+
status.EndorserServerStatus: []status.Code{
45+
status.Code(common.Status_SERVICE_UNAVAILABLE),
46+
status.Code(common.Status_INTERNAL_SERVER_ERROR),
47+
},
48+
status.OrdererServerStatus: []status.Code{
49+
status.Code(common.Status_SERVICE_UNAVAILABLE),
50+
status.Code(common.Status_INTERNAL_SERVER_ERROR),
51+
},
52+
status.EventServerStatus: []status.Code{
53+
status.Code(pb.TxValidationCode_DUPLICATE_TXID),
54+
status.Code(pb.TxValidationCode_ENDORSEMENT_POLICY_FAILURE),
55+
status.Code(pb.TxValidationCode_MVCC_READ_CONFLICT),
56+
status.Code(pb.TxValidationCode_PHANTOM_READ_CONFLICT),
57+
},
58+
// TODO: gRPC introduced retries in v1.8.0. This can be replaced with the
59+
// gRPC fail fast option, once available
60+
status.GRPCTransportStatus: []status.Code{
61+
status.Code(grpcCodes.Unavailable),
62+
},
63+
}
64+
65+
// ChannelClientRetryableCodes are the suggested codes that should be treated as
66+
// transient by fabric-sdk-go/api/apitxn.ChannelClient
67+
var ChannelClientRetryableCodes = map[status.Group][]status.Code{
68+
status.EndorserClientStatus: []status.Code{
69+
status.ConnectionFailed, status.EndorsementMismatch,
70+
},
71+
status.EndorserServerStatus: []status.Code{
72+
status.Code(common.Status_SERVICE_UNAVAILABLE),
73+
status.Code(common.Status_INTERNAL_SERVER_ERROR),
74+
},
75+
status.OrdererClientStatus: []status.Code{
76+
status.ConnectionFailed,
77+
},
78+
status.OrdererServerStatus: []status.Code{
79+
status.Code(common.Status_SERVICE_UNAVAILABLE),
80+
status.Code(common.Status_INTERNAL_SERVER_ERROR),
81+
},
82+
status.EventServerStatus: []status.Code{
83+
status.Code(pb.TxValidationCode_DUPLICATE_TXID),
84+
status.Code(pb.TxValidationCode_ENDORSEMENT_POLICY_FAILURE),
85+
status.Code(pb.TxValidationCode_MVCC_READ_CONFLICT),
86+
status.Code(pb.TxValidationCode_PHANTOM_READ_CONFLICT),
87+
},
88+
// TODO: gRPC introduced retries in v1.8.0. This can be replaced with the
89+
// gRPC fail fast option, once available
90+
status.GRPCTransportStatus: []status.Code{
91+
status.Code(grpcCodes.Unavailable),
92+
},
93+
}

pkg/retry/retry.go

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
/*
2+
Copyright SecureKey Technologies Inc. All Rights Reserved.
3+
4+
SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
// Package retry provides retransmission capabilities to fabric-sdk-go
8+
package retry
9+
10+
import (
11+
"time"
12+
13+
"github.com/hyperledger/fabric-sdk-go/pkg/status"
14+
)
15+
16+
// Opts defines the retry parameters
17+
type Opts struct {
18+
// Attempts the number retry attempts
19+
Attempts int
20+
// InitialBackoff the backoff interval for the first retry attempt
21+
InitialBackoff time.Duration
22+
// MaxBackoff the maximum backoff interval for any retry attempt
23+
MaxBackoff time.Duration
24+
// BackoffFactor the factor by which the InitialBackoff is exponentially
25+
// incremented for consecutive retry attempts.
26+
// For example, a backoff factor of 2.5 will result in a backoff of
27+
// InitialBackoff * 2.5 * 2.5 on the second attempt.
28+
BackoffFactor float64
29+
// RetryableCodes defines the status codes, mapped by group, returned by fabric-sdk-go
30+
// that warrant a retry. This will default to retry.DefaultRetryableCodes.
31+
RetryableCodes map[status.Group][]status.Code
32+
}
33+
34+
// Handler retry handler interface decides whether a retry is required for the given
35+
// error
36+
type Handler interface {
37+
Required(err error) bool
38+
}
39+
40+
// impl retry Handler implementation
41+
type impl struct {
42+
opts Opts
43+
retries int
44+
}
45+
46+
// New retry Handler with the given opts
47+
func New(opts Opts) Handler {
48+
if len(opts.RetryableCodes) == 0 {
49+
opts.RetryableCodes = DefaultRetryableCodes
50+
}
51+
return &impl{opts: opts}
52+
}
53+
54+
// WithDefaults new retry Handler with default opts
55+
func WithDefaults() Handler {
56+
return &impl{opts: DefaultOpts}
57+
}
58+
59+
// WithAttempts new retry Handler with given attempts. Other opts are set to default.
60+
func WithAttempts(attempts int) Handler {
61+
opts := DefaultOpts
62+
opts.Attempts = attempts
63+
return &impl{opts: opts}
64+
}
65+
66+
// Required determines if retry is required for the given error
67+
// Note: backoffs are implemented behind this interface
68+
func (i *impl) Required(err error) bool {
69+
if i.retries == i.opts.Attempts {
70+
return false
71+
}
72+
73+
s, ok := status.FromError(err)
74+
if ok && i.isRetryable(s.Group, s.Code) {
75+
time.Sleep(i.backoffPeriod())
76+
i.retries++
77+
return true
78+
}
79+
80+
return false
81+
}
82+
83+
// backoffPeriod calculates the backoff duration based on the provided opts
84+
func (i *impl) backoffPeriod() time.Duration {
85+
backoff, max := float64(i.opts.InitialBackoff), float64(i.opts.MaxBackoff)
86+
for j := 0; j < i.retries && backoff < max; j++ {
87+
backoff *= i.opts.BackoffFactor
88+
}
89+
if backoff > max {
90+
backoff = max
91+
}
92+
93+
return time.Duration(backoff)
94+
}
95+
96+
// isRetryable determines if the given status is configured to be retryable
97+
func (i *impl) isRetryable(g status.Group, c int32) bool {
98+
for group, codes := range i.opts.RetryableCodes {
99+
if g != group {
100+
continue
101+
}
102+
for _, code := range codes {
103+
if status.Code(c) == code {
104+
return true
105+
}
106+
}
107+
}
108+
return false
109+
}

0 commit comments

Comments
 (0)