github.com/palisadeinc/bor@v0.0.0-20230615125219-ab7196213d15/consensus/bor/heimdall/client.go (about) 1 package heimdall 2 3 import ( 4 "context" 5 "encoding/json" 6 "errors" 7 "fmt" 8 "io" 9 "net/http" 10 "net/url" 11 "sort" 12 "time" 13 14 "github.com/ethereum/go-ethereum/consensus/bor/clerk" 15 "github.com/ethereum/go-ethereum/consensus/bor/heimdall/checkpoint" 16 "github.com/ethereum/go-ethereum/consensus/bor/heimdall/span" 17 "github.com/ethereum/go-ethereum/log" 18 "github.com/ethereum/go-ethereum/metrics" 19 ) 20 21 var ( 22 // ErrShutdownDetected is returned if a shutdown was detected 23 ErrShutdownDetected = errors.New("shutdown detected") 24 ErrNoResponse = errors.New("got a nil response") 25 ErrNotSuccessfulResponse = errors.New("error while fetching data from Heimdall") 26 ) 27 28 const ( 29 stateFetchLimit = 50 30 apiHeimdallTimeout = 5 * time.Second 31 retryCall = 5 * time.Second 32 ) 33 34 type StateSyncEventsResponse struct { 35 Height string `json:"height"` 36 Result []*clerk.EventRecordWithTime `json:"result"` 37 } 38 39 type SpanResponse struct { 40 Height string `json:"height"` 41 Result span.HeimdallSpan `json:"result"` 42 } 43 44 type HeimdallClient struct { 45 urlString string 46 client http.Client 47 closeCh chan struct{} 48 } 49 50 type Request struct { 51 client http.Client 52 url *url.URL 53 start time.Time 54 } 55 56 func NewHeimdallClient(urlString string) *HeimdallClient { 57 return &HeimdallClient{ 58 urlString: urlString, 59 client: http.Client{ 60 Timeout: apiHeimdallTimeout, 61 }, 62 closeCh: make(chan struct{}), 63 } 64 } 65 66 const ( 67 fetchStateSyncEventsFormat = "from-id=%d&to-time=%d&limit=%d" 68 fetchStateSyncEventsPath = "clerk/event-record/list" 69 fetchCheckpoint = "/checkpoints/%s" 70 fetchCheckpointCount = "/checkpoints/count" 71 72 fetchSpanFormat = "bor/span/%d" 73 ) 74 75 func (h *HeimdallClient) StateSyncEvents(ctx context.Context, fromID uint64, to int64) ([]*clerk.EventRecordWithTime, error) { 76 eventRecords := make([]*clerk.EventRecordWithTime, 0) 77 78 for { 79 url, err := stateSyncURL(h.urlString, fromID, to) 80 if err != nil { 81 return nil, err 82 } 83 84 log.Info("Fetching state sync events", "queryParams", url.RawQuery) 85 86 ctx = withRequestType(ctx, stateSyncRequest) 87 88 response, err := FetchWithRetry[StateSyncEventsResponse](ctx, h.client, url, h.closeCh) 89 if err != nil { 90 return nil, err 91 } 92 93 if response == nil || response.Result == nil { 94 // status 204 95 break 96 } 97 98 eventRecords = append(eventRecords, response.Result...) 99 100 if len(response.Result) < stateFetchLimit { 101 break 102 } 103 104 fromID += uint64(stateFetchLimit) 105 } 106 107 sort.SliceStable(eventRecords, func(i, j int) bool { 108 return eventRecords[i].ID < eventRecords[j].ID 109 }) 110 111 return eventRecords, nil 112 } 113 114 func (h *HeimdallClient) Span(ctx context.Context, spanID uint64) (*span.HeimdallSpan, error) { 115 url, err := spanURL(h.urlString, spanID) 116 if err != nil { 117 return nil, err 118 } 119 120 ctx = withRequestType(ctx, spanRequest) 121 122 response, err := FetchWithRetry[SpanResponse](ctx, h.client, url, h.closeCh) 123 if err != nil { 124 return nil, err 125 } 126 127 return &response.Result, nil 128 } 129 130 // FetchCheckpoint fetches the checkpoint from heimdall 131 func (h *HeimdallClient) FetchCheckpoint(ctx context.Context, number int64) (*checkpoint.Checkpoint, error) { 132 url, err := checkpointURL(h.urlString, number) 133 if err != nil { 134 return nil, err 135 } 136 137 ctx = withRequestType(ctx, checkpointRequest) 138 139 response, err := FetchWithRetry[checkpoint.CheckpointResponse](ctx, h.client, url, h.closeCh) 140 if err != nil { 141 return nil, err 142 } 143 144 return &response.Result, nil 145 } 146 147 // FetchCheckpointCount fetches the checkpoint count from heimdall 148 func (h *HeimdallClient) FetchCheckpointCount(ctx context.Context) (int64, error) { 149 url, err := checkpointCountURL(h.urlString) 150 if err != nil { 151 return 0, err 152 } 153 154 ctx = withRequestType(ctx, checkpointCountRequest) 155 156 response, err := FetchWithRetry[checkpoint.CheckpointCountResponse](ctx, h.client, url, h.closeCh) 157 if err != nil { 158 return 0, err 159 } 160 161 return response.Result.Result, nil 162 } 163 164 // FetchWithRetry returns data from heimdall with retry 165 func FetchWithRetry[T any](ctx context.Context, client http.Client, url *url.URL, closeCh chan struct{}) (*T, error) { 166 // request data once 167 request := &Request{client: client, url: url, start: time.Now()} 168 result, err := Fetch[T](ctx, request) 169 170 if err == nil { 171 return result, nil 172 } 173 174 // attempt counter 175 attempt := 1 176 177 log.Warn("an error while trying fetching from Heimdall", "attempt", attempt, "error", err) 178 179 // create a new ticker for retrying the request 180 ticker := time.NewTicker(retryCall) 181 defer ticker.Stop() 182 183 const logEach = 5 184 185 retryLoop: 186 for { 187 log.Info("Retrying again in 5 seconds to fetch data from Heimdall", "path", url.Path, "attempt", attempt) 188 189 attempt++ 190 191 select { 192 case <-ctx.Done(): 193 log.Debug("Shutdown detected, terminating request by context.Done") 194 195 return nil, ctx.Err() 196 case <-closeCh: 197 log.Debug("Shutdown detected, terminating request by closing") 198 199 return nil, ErrShutdownDetected 200 case <-ticker.C: 201 request = &Request{client: client, url: url, start: time.Now()} 202 result, err = Fetch[T](ctx, request) 203 204 if err != nil { 205 if attempt%logEach == 0 { 206 log.Warn("an error while trying fetching from Heimdall", "attempt", attempt, "error", err) 207 } 208 209 continue retryLoop 210 } 211 212 return result, nil 213 } 214 } 215 } 216 217 // Fetch returns data from heimdall 218 func Fetch[T any](ctx context.Context, request *Request) (*T, error) { 219 isSuccessful := false 220 221 defer func() { 222 if metrics.EnabledExpensive { 223 sendMetrics(ctx, request.start, isSuccessful) 224 } 225 }() 226 227 result := new(T) 228 229 body, err := internalFetchWithTimeout(ctx, request.client, request.url) 230 if err != nil { 231 return nil, err 232 } 233 234 if body == nil { 235 return nil, ErrNoResponse 236 } 237 238 err = json.Unmarshal(body, result) 239 if err != nil { 240 return nil, err 241 } 242 243 isSuccessful = true 244 245 return result, nil 246 } 247 248 func spanURL(urlString string, spanID uint64) (*url.URL, error) { 249 return makeURL(urlString, fmt.Sprintf(fetchSpanFormat, spanID), "") 250 } 251 252 func stateSyncURL(urlString string, fromID uint64, to int64) (*url.URL, error) { 253 queryParams := fmt.Sprintf(fetchStateSyncEventsFormat, fromID, to, stateFetchLimit) 254 255 return makeURL(urlString, fetchStateSyncEventsPath, queryParams) 256 } 257 258 func checkpointURL(urlString string, number int64) (*url.URL, error) { 259 url := "" 260 if number == -1 { 261 url = fmt.Sprintf(fetchCheckpoint, "latest") 262 } else { 263 url = fmt.Sprintf(fetchCheckpoint, fmt.Sprint(number)) 264 } 265 266 return makeURL(urlString, url, "") 267 } 268 269 func checkpointCountURL(urlString string) (*url.URL, error) { 270 return makeURL(urlString, fetchCheckpointCount, "") 271 } 272 273 func makeURL(urlString, rawPath, rawQuery string) (*url.URL, error) { 274 u, err := url.Parse(urlString) 275 if err != nil { 276 return nil, err 277 } 278 279 u.Path = rawPath 280 u.RawQuery = rawQuery 281 282 return u, err 283 } 284 285 // internal fetch method 286 func internalFetch(ctx context.Context, client http.Client, u *url.URL) ([]byte, error) { 287 req, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil) 288 if err != nil { 289 return nil, err 290 } 291 292 res, err := client.Do(req) 293 if err != nil { 294 return nil, err 295 } 296 297 defer res.Body.Close() 298 299 // check status code 300 if res.StatusCode != 200 && res.StatusCode != 204 { 301 return nil, fmt.Errorf("%w: response code %d", ErrNotSuccessfulResponse, res.StatusCode) 302 } 303 304 // unmarshall data from buffer 305 if res.StatusCode == 204 { 306 return nil, nil 307 } 308 309 // get response 310 body, err := io.ReadAll(res.Body) 311 if err != nil { 312 return nil, err 313 } 314 315 return body, nil 316 } 317 318 func internalFetchWithTimeout(ctx context.Context, client http.Client, url *url.URL) ([]byte, error) { 319 ctx, cancel := context.WithTimeout(ctx, apiHeimdallTimeout) 320 defer cancel() 321 322 // request data once 323 return internalFetch(ctx, client, url) 324 } 325 326 // Close sends a signal to stop the running process 327 func (h *HeimdallClient) Close() { 328 close(h.closeCh) 329 h.client.CloseIdleConnections() 330 }