-
Notifications
You must be signed in to change notification settings - Fork 1.7k
/
request.go
206 lines (184 loc) · 7.66 KB
/
request.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
package v02
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"strconv"
"time"
"github.com/avast/retry-go/v4"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/smartcontractkit/chainlink-common/pkg/services"
"github.com/smartcontractkit/chainlink/v2/core/logger"
"github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ocr2keeper/evmregistry/v21/encoding"
"github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ocr2keeper/evmregistry/v21/mercury"
"github.com/smartcontractkit/chainlink/v2/core/utils"
)
const (
mercuryPathV02 = "/client?" // only used to access mercury v0.2 server
retryDelay = 500 * time.Millisecond
totalAttempt = 3
contentTypeHeader = "Content-Type"
authorizationHeader = "Authorization"
timestampHeader = "X-Authorization-Timestamp"
signatureHeader = "X-Authorization-Signature-SHA256"
)
type MercuryV02Response struct {
ChainlinkBlob string `json:"chainlinkBlob"`
}
type client struct {
services.StateMachine
mercuryConfig mercury.MercuryConfigProvider
httpClient mercury.HttpClient
threadCtrl utils.ThreadControl
lggr logger.Logger
}
func NewClient(mercuryConfig mercury.MercuryConfigProvider, httpClient mercury.HttpClient, threadCtrl utils.ThreadControl, lggr logger.Logger) *client {
return &client{
mercuryConfig: mercuryConfig,
httpClient: httpClient,
threadCtrl: threadCtrl,
lggr: lggr,
}
}
func (c *client) DoRequest(ctx context.Context, streamsLookup *mercury.StreamsLookup, pluginRetryKey string) (encoding.PipelineExecutionState, encoding.UpkeepFailureReason, [][]byte, bool, time.Duration, error) {
resultLen := len(streamsLookup.Feeds)
ch := make(chan mercury.MercuryData, resultLen)
if len(streamsLookup.Feeds) == 0 {
return encoding.NoPipelineError, encoding.UpkeepFailureReasonInvalidRevertDataInput, [][]byte{}, false, 0 * time.Second, fmt.Errorf("invalid revert data input: feed param key %s, time param key %s, feeds %s", streamsLookup.FeedParamKey, streamsLookup.TimeParamKey, streamsLookup.Feeds)
}
for i := range streamsLookup.Feeds {
// TODO (AUTO-7209): limit the number of concurrent requests
i := i
c.threadCtrl.Go(func(ctx context.Context) {
c.singleFeedRequest(ctx, ch, i, streamsLookup)
})
}
var reqErr error
var retryInterval time.Duration
results := make([][]byte, len(streamsLookup.Feeds))
retryable := true
allSuccess := true
// in v0.2, use the last execution error as the state, if no execution errors, state will be no error
state := encoding.NoPipelineError
for i := 0; i < resultLen; i++ {
m := <-ch
if m.Error != nil {
reqErr = errors.Join(reqErr, m.Error)
retryable = retryable && m.Retryable
allSuccess = false
if m.State != encoding.NoPipelineError {
state = m.State
}
continue
}
results[m.Index] = m.Bytes[0]
}
if retryable && !allSuccess {
retryInterval = mercury.CalculateRetryConfigFn(pluginRetryKey, c.mercuryConfig)
}
// only retry when not all successful AND none are not retryable
return state, encoding.UpkeepFailureReasonNone, results, retryable && !allSuccess, retryInterval, reqErr
}
func (c *client) singleFeedRequest(ctx context.Context, ch chan<- mercury.MercuryData, index int, sl *mercury.StreamsLookup) {
var httpRequest *http.Request
var err error
q := url.Values{
sl.FeedParamKey: {sl.Feeds[index]},
sl.TimeParamKey: {sl.Time.String()},
}
mercuryURL := c.mercuryConfig.Credentials().LegacyURL
reqUrl := fmt.Sprintf("%s%s%s", mercuryURL, mercuryPathV02, q.Encode())
c.lggr.Debugf("request URL for upkeep %s feed %s: %s", sl.UpkeepId.String(), sl.Feeds[index], reqUrl)
httpRequest, err = http.NewRequestWithContext(ctx, http.MethodGet, reqUrl, nil)
if err != nil {
ch <- mercury.MercuryData{Index: index, Error: err, Retryable: false, State: encoding.InvalidMercuryRequest}
return
}
ts := time.Now().UTC().UnixMilli()
signature := mercury.GenerateHMACFn(http.MethodGet, mercuryPathV02+q.Encode(), []byte{}, c.mercuryConfig.Credentials().Username, c.mercuryConfig.Credentials().Password, ts)
httpRequest.Header.Set(contentTypeHeader, "application/json")
httpRequest.Header.Set(authorizationHeader, c.mercuryConfig.Credentials().Username)
httpRequest.Header.Set(timestampHeader, strconv.FormatInt(ts, 10))
httpRequest.Header.Set(signatureHeader, signature)
// in the case of multiple retries here, use the last attempt's data
state := encoding.NoPipelineError
retryable := false
sent := false
retryErr := retry.Do(
func() error {
var httpResponse *http.Response
var responseBody []byte
var blobBytes []byte
retryable = false
if httpResponse, err = c.httpClient.Do(httpRequest); err != nil {
c.lggr.Warnf("at block %s upkeep %s GET request fails for feed %s: %v", sl.Time.String(), sl.UpkeepId.String(), sl.Feeds[index], err)
retryable = true
state = encoding.MercuryFlakyFailure
return err
}
defer httpResponse.Body.Close()
if responseBody, err = io.ReadAll(httpResponse.Body); err != nil {
state = encoding.InvalidMercuryResponse
return err
}
switch httpResponse.StatusCode {
case http.StatusNotFound, http.StatusInternalServerError, http.StatusBadGateway, http.StatusServiceUnavailable, http.StatusGatewayTimeout:
c.lggr.Warnf("at block %s upkeep %s received status code %d for feed %s", sl.Time.String(), sl.UpkeepId.String(), httpResponse.StatusCode, sl.Feeds[index])
retryable = true
state = encoding.MercuryFlakyFailure
return errors.New(strconv.FormatInt(int64(httpResponse.StatusCode), 10))
case http.StatusOK:
// continue
default:
state = encoding.InvalidMercuryRequest
return fmt.Errorf("at block %s upkeep %s received status code %d for feed %s", sl.Time.String(), sl.UpkeepId.String(), httpResponse.StatusCode, sl.Feeds[index])
}
c.lggr.Debugf("at block %s upkeep %s received status code %d from mercury v0.2 with BODY=%s", sl.Time.String(), sl.UpkeepId.String(), httpResponse.StatusCode, hexutil.Encode(responseBody))
var m MercuryV02Response
if err = json.Unmarshal(responseBody, &m); err != nil {
c.lggr.Warnf("at block %s upkeep %s failed to unmarshal body to MercuryV02Response for feed %s: %v", sl.Time.String(), sl.UpkeepId.String(), sl.Feeds[index], err)
state = encoding.MercuryUnmarshalError
return err
}
if blobBytes, err = hexutil.Decode(m.ChainlinkBlob); err != nil {
c.lggr.Warnf("at block %s upkeep %s failed to decode chainlinkBlob %s for feed %s: %v", sl.Time.String(), sl.UpkeepId.String(), m.ChainlinkBlob, sl.Feeds[index], err)
state = encoding.InvalidMercuryResponse
return err
}
ch <- mercury.MercuryData{
Index: index,
Bytes: [][]byte{blobBytes},
Retryable: false,
State: encoding.NoPipelineError,
}
sent = true
return nil
},
// only retry when the error is 404 Not Found, 500 Internal Server Error, 502 Bad Gateway, 503 Service Unavailable, 504 Gateway Timeout
retry.RetryIf(func(err error) bool {
return err.Error() == fmt.Sprintf("%d", http.StatusNotFound) || err.Error() == fmt.Sprintf("%d", http.StatusInternalServerError) || err.Error() == fmt.Sprintf("%d", http.StatusBadGateway) || err.Error() == fmt.Sprintf("%d", http.StatusServiceUnavailable) || err.Error() == fmt.Sprintf("%d", http.StatusGatewayTimeout)
}),
retry.Context(ctx),
retry.Delay(retryDelay),
retry.Attempts(totalAttempt),
)
if !sent {
ch <- mercury.MercuryData{
Index: index,
Bytes: [][]byte{},
Retryable: retryable,
Error: fmt.Errorf("failed to request feed for %s: %w", sl.Feeds[index], retryErr),
State: state,
}
}
}
func (c *client) Close() error {
return c.StopOnce("v02_request", func() error {
c.threadCtrl.Close()
return nil
})
}