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  }